map in clojure giving unexpected result

Viewed 185

Using map in clojure, checking whether a string contains uppercase character.

(map #(= (clojure.string/upper-case %) %) "Hello") 

The expected result.

(true false false false false)

unfortunately, the result is unexpected.

(false false false false false)

I did an experiment when I replace "H" in the first "%", the result is still unexpected.

(map #(= (clojure.string/upper-case "H") %) "Hello") 

(false false false false false)

When I replace "H" in the second "%", the result is changed, it is an expected result.

(map #(= (clojure.string/upper-case %) "H) "Hello") 

(true false false false false)

What's wrong with that? Please feel free to comment.

5 Answers

As others have pointed out, comparing a character to a string will not work. Comparing strings will work:

(map #(= (clojure.string/upper-case %) (str %)) "Hello")
=> (true false false false false)

However this is more direct:

(map #(Character/isUpperCase %) "Hello")
=> (true false false false false)

You can't compare String with a char. Sequence made out of string is a list of chars.

(seq "Hello")
;; => (\H \e \l \l \o)
(map class "Hello")
;; => (java.lang.Character java.lang.Character java.lang.Character java.lang.Character java.lang.Character)

So in your case your test boils down to

(= "H" \H) ;; => false

which is false.

To make it work, try one of the following:

;; compare strings
(map #(= (clojure.string/upper-case %) (str %)) "Hello")
;; => (true false false false false)

;; compare chars
(map #(= (first (clojure.string/upper-case %)) %) "Hello")
;; => (true false false false false)

It is because you assume wrongly that map dissects the string "Hello" into ("H" "e" "l" "l" "o") but this it not the case!

Try:

(map identity "Hello") ;; => (\H \e \l \l \o)

So map dissects a string into a list of characters and not a list of single character strings.

So your (map #(= (clojure.string/upper-case %) %) "Hello") is comparing (clojure.string/upper-case \H) which results in "H" with \H. Since the one is a single character string, the other however is a character, the result is false - because of type difference.

(map #(= (clojure.string/upper-case %) (str %)) "Hello")

However does what you expected from the original code. It returns:

(true false false false false)

Function

The most efficient way might be to use regex:

(defn has-upper-case? [s] (boolean (re-find #"[A-Z]" s)))

Less strictly (no boolean returning, but either the first upper single character string of the string from left to right or nil if not found any):

(defn has-upper-case? [s] (re-find #"[A-Z]" s))

You're being caught by undocumented behaviour of clojure.string/upper-case:

  • It works on any object, not just on strings.
  • It always returns a string.

So, for example,

(clojure.string/upper-case +)
=> "CLOJURE.CORE$_PLUS_@19CFE007"

If you look at the source code, you see that it applies .toString before .toUpperCase.

So it works on single characters too. But it returns a string, not - what I expected - a character:

(clojure.string/upper-case 1)
=> "1"

The remedy is to apply upper-case to the string as a whole, then use map with = to compare the original string to the converted one, character by character:

(#(map = (clojure.string/upper-case %) %) "Hello")
=> (true false false false false)

Or, using (fn [...] ...) instead of #(...),

((fn [coll] (map = (clojure.string/upper-case coll) coll)) "Hello")
=> (true false false false false)
Related