Templating in clj-pdf - clojure

I'm using clj-pdf to generate pdf files. As the README file suggests, the library provides some rudimentary templating options.
For example, given a vector of maps, such as:
(def employees
[{:country "Germany",
:place "Nuremberg",
:occupation "Engineer",
:name "Neil Chetty"}
{:country "Germany",
:place "Ulm",
:occupation "Engineer",
:name "Vera Ellison"}])
and a template
(def employee-template
(template
[:paragraph
[:heading $name]
[:chunk {:style :bold} "occupation: "] $occupation "\n"
[:chunk {:style :bold} "place: "] $place "\n"
[:chunk {:style :bold} "country: "] $country
[:spacer]]))
The following output will be produced:
(employee-template employees)
([:paragraph [:heading "Neil Chetty"]
[:chunk {:style :bold} "occupation: "] "Engineer" "\n"
[:chunk {:style :bold} "place: "] "Nuremberg" "\n"
[:chunk {:style :bold} "country: "] "Germany" [:spacer]]
[:paragraph [:heading "Vera Ellison"]
[:chunk {:style :bold} "occupation: "] "Engineer" "\n"
[:chunk {:style :bold} "place: "] "Ulm" "\n"
[:chunk {:style :bold} "country: "] "Germany" [:spacer]])
However, I'm wondering how to use this template in pdf function. When I use
(pdf
[[:heading "Heading 1"]
[:table
{:width 100 :border false :cell-border false :widths [30 70] :offset 35}
[[:cell {:align :right}
(employee-template employees)]
[:cell "dummy"]]]
[:heading "Heading 2"]]
"output.pdf")
I got a invalid tag exception.
If I change (employee-template employees) to (first (employee-template employees)), it works however not as I expect. What is the correct way to use a template?

This works. Generate a new cell for each employee.
(pdf
[[:heading "Heading 1"]
[:table {:width 100 :border false :cell-border false :widths [30 70] :offset 35}
(for [e (employee-template employees)]
[:cell {:align :right} e])]
[:heading "Heading 2"]]
"output.pdf")

Use apply and concat maybe?
(pdf
(apply concat
[[:heading "Heading 1"]]
(employee-template employees)
[[:heading "Heading 2"]])
"output.pdf")
I think there should be a better choice to do this,the concat may not be efficient.

Related

Clojure hiccup vanishing key namespaces

I'm printing out a map's key values to html, and the key namespaces are vanishing, which I don't want.
layout below calls hiccup's html5 to render:
(layout (str "Path " (:path/title path))
[:h1 "Title: " (:title path) slug]
[:p (str path)] ; prints "{:db/id 17592186045542, :path/title "sdf"}"
(println (keys path)) ; prints in terminal "(:db/id :path/title)"
[:p (keys path)] ; prints "idtitle"
(for [[k v] path] [:p k " " v]) ; prints "id 17592186045542" /n "title sdf"
(map (fn [[k v]] [:p k " " v]) path)))) ; same as above
In both (keys path), and the for & map calls, the ":db/" and ":path/" namespaces of the keys are not rendered. Why?
I suppose the keys are being implicitly named, unlike the good cases where you explicitly use str on them.
Maybe you should use
[:p (str k) " " (str v)]
Or simply:
[:p (str/join " " [k v])]

Turning a list of strings into a single string in clojure

For example, let's say I have a list
'("The" " " "Brown" " " "Cow")
I want to turn this into
"The Brown Cow"
Is there a command in clojure that does this?
I would rather use apply for that:
(apply str '("The" " " "Brown" " " "Cow"))
Since it calls the function just once, it is much more efficient for large collections:
(defn join-reduce [strings] (reduce str strings))
(defn join-apply [strings] (apply str strings))
user> (time (do (join-reduce (repeat 50000 "hello"))
nil))
"Elapsed time: 4708.637673 msecs"
nil
user> (time (do (join-apply (repeat 50000 "hello"))
nil))
"Elapsed time: 2.281443 msecs"
nil
As Chris mentioned you can just use clojure.string/join
another way without using a library (assuming you don't want any spaces.) is:
(reduce str '("The" " " "Brown" " " "Cow"))
will return
"The Brown Cow"
str takes a list of things and turns them into one string. You can even do this: (str "this is a message with numbers " 2 " inside")
Please, people, just say 'No' to the lists! So simple with vectors:
(ns clj.demo
(:require [clojure.string :as str] ))
(def xx [ "The" " " "Brown" " " "Cow" ] )
(prn (str/join xx))
;=> "The Brown Cow"
Quoted lists like:
'( "The" " " "Brown" " " "Cow" )
are much harder to type and read, and also more error-prone than a simple vector literal:
[ "The" " " "Brown" " " "Cow" ]
Also cut/paste is much easier, as you don't need to worry about adding the quote or forgetting to add it.
Note that (str/join ... can also accept an optional separator as the first argument. You can either use the 1-char string like the first example or a "literal" as in the 2nd example:
(ns clj.demo
(:require
[clojure.string :as str]
[tupelo.core :as t] ))
(let [words ["The" "Brown" "Cow"]]
(t/spyx (str/join " " words))
(t/spyx (str/join \space words)))
with results
(str/join " " words) => "The Brown Cow"
(str/join \space words) => "The Brown Cow"

Anonymous function with rest parameter throwing NullPointerException

Here's the function
(#(
(println (str "first: " %1))
(println (str "second: " %2))
(println (str "rest: " (clojure.string/join ", " %&))))
"f" "s" "x" "y" "z")
When running this in cider I get the desired result but at the end I see that I am also getting a NullPointerException.
It seems that this form of anonymous function has some issues with destructuring.
Because, when I try the following form of anonymous function, it works.
((fn [f s & rest]
(println (str "first: " f))
(println (str "second: " s))
(println (str (clojure.string/join ", " rest))))
"f" "s" "x" "y" "z")
Can someone explain why is this happening ?
You need a do:
(#(do
(println (str "first: " %1))
(println (str "second: " %2))
(println (str "rest: " (clojure.string/join ", " %&))))
"f" "s" "x" "y" "z")
Without the do, you're trying to invoke the result of the first println, (i.e. nil) on the remaining elements of the list. fn has an implicit do.
For a minimal case, compare ((println)) and (do (println))

How do I join string set into a single string with positions prepended?

Assume I have this
(def base ["one" "two" "three"])
I want to convert it to:
1. one
2. two
3. three
(aka 1. one \n2. two \n3. three)
with join, I am not sure I can append a counter before joining:
(clojure.string/join " \n" base)
=> "one \ntwo \nthree"
and with doseq or similar, plus an atom, I do get individual strings but then will have to concatenate later on, something like
(def base ["one" "two" "three"])
(def pos (atom 0))
(defn add-pos
[base]
(for [b base]
(do
(swap! pos inc)
(str #pos ". " b))))
(let [pos-base (add-pos base)]
(clojure.string/join " \n" pos-base))
=> "1. one \n2. two \n3. three"
While it works, I don't know if using an atom with a for statement is he best way to do this, it doesn't look very clojure-esque.
Is there a better way to do this please?
That's a job for keep-indexed:
user> (keep-indexed #(str (inc %1) ". " %2) ["one" "two" "three"])
("1. one" "2. two" "3. three")
user> (clojure.string/join "\n"
(keep-indexed
#(str (inc %1) ". " %2)
["one" "two" "three"]))
"1. one\n2. two\n3. three"
A minor alternative to schaueho's keep-indexed would be map-indexed (spotting a pattern?)
(def base ["one" "two" "three"])
(defn numbered-list [s]
(->> s
(map-indexed #(str (inc %1) ". " %2))
(interpose \newline)
(apply str)))
(numbered-list base) ; => "1. one\n2. two\n3. three"
Clearly a job for interleave.
(->> (interleave (rest (range)) (repeat ". ") base (repeat " \n"))
(apply str))
;-> "1. one \n2. two \n3. three \n"

Clojure changing binding local's value depending on conditions

What is the idomatic way to change a local depending on conditions like below? Here I am changing value of x depending on some conditions.
(defn person-story
[person]
(let [x (str "My name is " (:firstname person) " " (:lastname person) "." )
x (if (:showaddress person) (str x " I live in " (:address person) ".") x)
x (if (:showage person) (str x " I am " (:age person) " yrs old. ") x)
x (if (seq (:hobby person)) (str x " I like " (clojure.string/join ", " (:hobby person)) ".") x)]
x))
(person-story {:firstname "John" :lastname "Doe" :age 45 :showage false :address "67 Circe Ave" :showaddress true :hobby ["movie" "music" "money"]})
And that would output:
"My name is John Doe. I live in 67 Circe Ave. I like movie, music, money."
If I were to do it in java, i would have done something like:
StringBuilder sb = new StringBuilder("My name is ");
sb.append(person.get("firstname")).append(" ");
sb.append(person.get("lastname")).append(" ");
if (showaddress) sb.append("I live in ").append(person.get("address")).append(" ");
if (showage) sb.append("I am ").append(person.get("age")).append(" yrs old. ");
List<String> hobbies = person.get("hobby");
if ( hobbies != null && !hobbies.isEmpty()) sb.append("I like "). append(StringUtils.join(hobbies, ", "));
return sb.toString()
What is the best way to achieve the same thing that I am achieving above in clojure? Sorry for the title, I could not come up with a better one.
Solutions
Thank you xsc and amalloy, both answers were great. I accepted amalloy's answer since it showed a totally new way of solving the problem, but upvoted both.
Here are the snippets for solution in both suggested ways:
amalloy's method:
(defn person-story2 [person]
(let [tests [[:showaddress #(format " I live in %s." (:address %))]
[:showage #(format " I am %s yrs old." (:age %))]
[(comp seq :hobbies) #(format " I like %s." (clojure.string/join ", " (:hobbies %)))]]]
(apply str (format "My name is %s %s." (:firstname person) (:lastname person))
(for [[test f] tests
:when (test person)]
(f person)))))
(person-story2 {:firstname "John" :lastname "Doe" :showage true :age 50 :showaddress true :address "Universal Studios" :hobbies ["movies" "music" "money"]})
output:
"My name is John Doe. I live in Universal Studios. I am 50 yrs old. I like movies, music, money."
xsc's method:
(defn person-story
[{:keys [firstname lastname address showaddress age showage hobbies] :as person}]
(cond->
(str "My name is " firstname " " lastname ". ")
showaddress (str "I live in " address ". ")
showage (str "I am " age " yrs old. ")
(seq hobbies) (str "I like " (clojure.string/join ", " hobbies))))
(person-story {:firstname "John" :lastname "Doe" :showage false :age 50 :address "Universal Studios" :showaddress true :hobbies ["movies" "music" "money"]})
output:
"My name is John Doe. I live in Universal Studios. I like movies, music, money"
Since Clojure 1.5 there are cond->/cond->> that work just like ->/->> with conditionals determining whether a single step is performed:
(cond->
(str "My name is " (:firstname p) " " (:lastname p) ".")
(:showaddress p) (str "I live in " (:address p) ".")
(:showage p) (str "I am " (:age p) " yrs old.")
...)
This would be the idiomatic solution for conditional string building. Alternatively, you could use something along the lines of:
(clojure.string/join
[(str "My name is " (:firstname p) " " (:lastname p) ".")
(if (:showaddress p)
(str "I live in " (:address p) "."))
...])
This uses the fact that nil will be ignored when joining strings.
To avoid repeating anything useless (eg, if conditions, or the x you are threading through everything), define a list of the tests you want to run, and the functions to apply if the tests succeed. Then you can just reduce over that list. In the simple case of building a string via repeated calls to str, you can cut out the middleman a bit and just call str yourself, via apply:
(defn person-story [person]
(let [tests [[:showaddress #(format " I live in %s." (:address %))]
[:showage #(format " I am %s yrs old." (:age %))]
[(comp seq :hobby) #(format " I like %s." (clojure.string/join ", " (:hobby %)))]]]
(apply str (format "My name is %s %s." (:firstname person) (:lastname person))
(for [[test f] tests
:when (test person)]
(f person)))))