Skip to content

Instantly share code, notes, and snippets.

@ericnormand
Last active August 3, 2020 16:08
Show Gist options
  • Save ericnormand/9d0e28695e84efc27c36970ae1462b9c to your computer and use it in GitHub Desktop.
Save ericnormand/9d0e28695e84efc27c36970ae1462b9c to your computer and use it in GitHub Desktop.
388 - PurelyFunctional.tv Newsletter -

Title Case

In English, titles of books get their own type of capitalization called title case. In title case, major words are capitalized and minor words are in lower case. Most words are major words. Minor words are:

  • three letters or shorter AND
  • conjunctions, articles, or prepositions

And, finally, the first word of a title is always capitalized, even if it is a minor word.

For a better, and more complete, summary of the nuances of title case, see the APA guide here: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case

Your task is to write a function that implements title case. The string you get will already have proper nouns capitalized, and you can otherwise assume you only need to capitalize words, not lowercase them.

Here's an example:

(title-case "the hobbit") ;=> "The Hobbit"
(title-case "the fellowship of the ring") ;=> "The Fellowship of the Ring"
(title-case "the two towers") ;=> "The Two Towers"
(title-case "the return of the king") ;=> "The Return of the King"

Here is a list of minor words you can use:

["and" "as" "but" "for" "if" "nor" "or" "so" "yet" "a" "an" "the" "as" "at" "by" "for" "in" "of" "off" "on" "per" "to" "up" "via"]

There are probably more minor words, but that's a good list to start with.

Also, please take this function as far as you'd like, using any title case rules as you see fit. The English language has no definitive standard, so many places define their own rules that may differ.

Thanks to this site for the challenge idea where it is considered Medium level in Python.

Email submissions to eric@purelyfunctional.tv before August 02, 2020. You can discuss the submissions in the comments below.

;; Handles:
;; - normal case, first cap, major/minor criteria
;; - hyphens between words
;; - hyphenated words
;; - terminal punctuation . ! ? ;
(require '(clojure [string :as s]))
;; minor words are conjunctions, articles, and (short) prepositions
(def c-a-plist (set ["and" "as" "but" "for" "if" "nor" "or" "so" "yet" "a" "an" "the"
"as" "at" "by" "for" "in" "of" "off" "on" "per" "to" "up" "via"]))
;; handle hyphenated words
(defn dohyp [w]
(s/join "-" (map s/capitalize (s/split w #"-"))))
;; upcase major words
(defn docase [w]
(if (and (> (count w) 2) (contains? (set w) \-))
(dohyp w)
(if (contains? c-a-plist w) w (s/capitalize w))))
;; title-case 1 segment by capitalizing first and major words
(defn tc1 [s]
(let [[f & r] (s/split s #" ")]
(s/join " " (cons (s/capitalize f) (map docase r)))))
;; break string into punctuation separated segments
(defn tc-punc [s p]
(s/join (apply str (rest p))
(map tc1 (map s/trim (s/split s (re-pattern p))))))
;; apply title-casing to segments defined by punctuation boundaries
(defn title-case [s]
(reduce tc-punc (s/lower-case s) ["\\. " "\\! " "\\? " "\\; " "\\ - "]))
;; tests
[
(= "Handle First Cap and Major / Minor"
(title-case "handle first cap and major / Minor"))
(= "This Title - Has a Hyphen Between Words."
(title-case "this title - has a hyphen between words."))
(= "Handle a Hyphenated-Word Correctly"
(title-case "handle a hyphenated-word correctly"))
(= "Last Dot."
(title-case "last dot."))
(= "Handle Any of a Few Minor Words"
(title-case "handle any of a few minor words"))
(= "All Together-Now - Will It Work? I Do Not as yet Know; I Do Care. I Do! Do You?"
(title-case "all together-now - will it work? i do not as yet know; i do care. i do! do you?"))
]
(ns zugnush.challenge-388
(:require [clojure.string :as str]))
(def minor-words
(set ["and" "as" "but" "for" "if" "nor" "or" "so" "yet" "a" "an" "the" "as" "at" "by" "for" "in" "of" "off" "on" "per" "to" "up" "via"]))
(defn title-case
[s]
(let [words (str/split s #"\W")]
(->> words
rest
(map #(if (minor-words %) % (str/capitalize %)))
(cons (str/capitalize (first words)))
(str/join " "))))
(require '[clojure.string :as string])
(def minor-words #{"A" "An" "And" "As" "At" "But" "By" "For" "If" "In" "Nor"
"Of" "Off" "On" "Or" "Per" "So" "The" "To" "Up" "Via" "Yet"})
(defn title-case [s]
(->> (string/split (str s) #"\s")
(map string/capitalize)
(map-indexed (fn [idx word]
(if (and (not (zero? idx)) (minor-words word))
(string/lower-case word)
word)))
(string/join #" ")))
;; Another version using RegEx
(defn title-case [s]
(let [re (re-pattern (str "(?!^)(" (string/join #"|" minor-words) ") "))]
(-> (string/lower-case s)
(string/replace #"\b." #(.toUpperCase %1))
(string/replace re #(.toLowerCase (first %1))))))
(defn ->title-case [data]
(if (and (not (empty? data))
(not (nil? data)))
(let [f (str/capitalize (first data))
r (map str/lower-case (next data))]
(str f (str/join "" r)))))
(defn is-minor-word? [word]
(let [minor-words (set ["and" "as" "but" "for" "if" "nor" "or" "so" "yet" "a" "an" "the" "as" "at" "by"
"for" "in" "of" "off" "on" "per" "to" "up" "via"])]
(if (minor-words word) true false)))
(defn title-case [sen]
(let [sen (str/split sen #" ")
first-word (->title-case (first sen))
rest-of-sen (->> sen
next
(map (fn [x] (if (is-minor-word? x) x (->title-case x))))
(str/join " "))]
(str first-word " " rest-of-sen)))
(= (title-case "the hobbit") "The Hobbit")
(= (title-case "the fellowship of the ring") "The Fellowship of the Ring")
(= (title-case "the two towers") "The Two Towers")
(= (title-case "the return of the king") "The Return of the King")
(def minor-words #{"a" "an" "and" "as" "at" "but" "by" "for" "if" "in" "nor" "of"
"off" "on" "or" "per" "so" "the" "to" "up" "via" "yet"})
(defn title-case [s]
(let [words (clojure.string/split s #" ")
title-rest (map #(or (minor-words %) (clojure.string/capitalize %)) (rest words))
title-all (cons (clojure.string/capitalize (first words)) title-rest)]
(clojure.string/join " " title-all)))
@ninjure
Copy link

ninjure commented Jul 31, 2020

(require '[clojure.string :as string])

(def minor-words #{"A" "An" "And" "As" "At" "But" "By" "For" "If" "In" "Nor"
                   "Of" "Off" "On" "Or" "Per" "So" "The" "To" "Up" "Via" "Yet"})

(defn title-case [s]
  (->> (string/split (str s) #"\s")
       (map string/capitalize)
       (map-indexed (fn [idx word]
                      (if (and (not (zero? idx)) (minor-words word))
                        (string/lower-case word)
                        word)))
       (string/join #" ")))

;; Another version using RegEx
(defn title-case [s]
  (let [re (re-pattern (str "(?!^)(" (string/join #"|" minor-words) ") "))]
    (-> (string/lower-case s)
        (string/replace #"\b." #(.toUpperCase %1))
        (string/replace re #(.toLowerCase (first %1))))))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment