formater, ikke konkatiner


torsdag 7. april 2011 Clojure Polyglot C#

Mye av det programmene våre gjør er å behandle tekst (og da mener jeg “våre” som i “oss utviklere og alt vi finner på å lage”). Noe av det første vi lærer som ferske hackere er å konkatinere strenger – dvs. å slå sammen to biter med tekst.

Og i forhold til konstruksjon av tekststrenger stopper det der for veldig mange. Du kan egentlig klare deg ganske lenge med enkel konkatinering, men det er ikke alltid det mest elegante!

Siden tidenes morgen (dvs. 50-tallet) har programmeringsspråk inkludert diverse metoder for å formatere tekst. Den aller mest kjente og brukte funksjonen heter printf, og dukket først opp i språket C – men den ble egentlig arvet fra BCPL, hvor den het writef (men hvem har vel egentlig hørt om BCPL?). Siden har printf inspirert andre, og i dag finner du dens etterkommere i nær sagt alle programmeringsspråk.

Format i .Net

Ett av disse språkene er C#, hvor vi kan bruke formatstrenger i mange sammenhenger. Den vanligste bruken er en statisk metode på String-klassen som heter Format. Her er et banalt eksempel:

10 int age = 35;
11 string givenName = "Torbjørn";
12 string surname = "Marø";
13 
14 // Konkatinering på den vanlige måten
15 
16 string displayName = surname + ", " + givenName + " (" + age + ")";
17 
18 // mer elegant med en formatstreng
19 
20 string displayName = String.Format("{0}, {1} ({2})",
21                                    surname,
22                                    givenName,
23                                    age);

Det er mye lettere å innføre feil ved vanlig konkatinering enn ved bruk av format, og det er også (i alle fall for et trent øye) mye lettere å se hva resultatet kommer til å bli når man ser en formatstreng.

String.Format er mye mer avansert enn det kan virke ved første øyekast. Men det er sjelden vi benytt mer enn det mest elementære, og jeg må nesten alltid slå opp i dokumentasjonen om jeg trenger å formatere f.eks. tall eller en dato på en bestemt måte.

I det neste eksempelet bruker jeg Console.Writeline, som også aksepterer en formatstreng. Her har jeg spesifisert at tallet skal formateres som penger, og at datoen skal vises som dagens navn, dagens dato (med ett siffer om mindre enn 10) og månedens navn.

1 double money = 12345.6789;
2 Console.WriteLine("Pris ({0}): {1:C} | {2:dddd dd. MMMM}",
3                   CultureInfo.CurrentCulture,
4                   money,
5                   DateTime.Now);
6 
7 // output: "Pris (nb-NO): kr 12 345,68 | torsdag 7. april"

Format i Clojure (og Java)

I Clojure har vi også en format-funksjon, og en tilsvarende printf-funksjon som tar en formatstreng som input og skriver til standard out. Under panseret bruker disse funksjonene java.util.Formatter, som er inspirert av C’s tradisjonelle printf.

Her er noen eksempler på format i bruk, og strengene de genererer..

10 (format "Hello, %s!" "world")                      ;=> Hello, world!
11 (format "%s = %s" "SomeKey" "SomeValue")           ;=> SomeKey = SomeValue
12 (format "%1$s + %1$s = %2$s" 2 4)                  ;=> 2 + 2 = 4
13 (format "e = %+.4f" 2.718281828459045)             ;=> e = +2,7183
14 (format ":%-20s:" "right padding")                 ;=> :right padding       :
15 (format ":%20s:" "left padding")                   ;=> :        left padding:
16 (format "%b %b %b %b" true false nil "something")  ;=> true false false true
17 (format "#%02x%02x%02x;" 255 0 125)                ;=> #ff007d;
18 (format "%s" (Date.))                              ;=> Thu Apr 07 21:04:29 CEST 2011
19 (format "Offset: %tz" (Date.))                     ;=> Offset: +0100
20 (format "Time zone: %tZ" (Date.))                  ;=> Time zone: CEST
21 (format "%1$tH:%1$tM:%1$tS" (Date.))               ;=> 21:04:29
22 (format "%1$tl:%1$tM %1$tp" (Date.))               ;=> 9:04 pm
23 (format "Unix time: %ts" (Date.))                  ;=> Unix time: 1302203069
24 (format "%1$td/%1$tm-%1$ty" (Date.))               ;=> 07/04-11
25 (format "%1$td. %1$tb %1$ty" (Date.))              ;=> 07. apr 11
26 (format "%1$td. %1$tB %1$tY" (Date.))              ;=> 07. april 2011
27 (format "ISO 8601: %tF" (Date.))                   ;=> ISO 8601: 2011-04-07

(Dette eksempelet ble generert av et Clojure-skript. Hvis du er interesert finner du det her.)

Interpolering

Variabel-interpolering er en lignende teknikk man finner i mange dynamiske språk, som Perl, PHP og Ruby. Her kan tekst-strenger direkte inneholde referanser til variabler eller mer komplekse uttrykk, og språket parser og evaluerer dem og erstatter dem med teksten.

Her er et lite eksempel fra Ruby..

1 given = 'James'
2 surname = 'Bond'
3 
4 puts "My name is #{surname}, #{given} #{surname}."
5 
6 # => My name is Bond, James Bond.

Clojure har også en interpolerings-funksjon i contrib-bibloteket. Den heter <<, og i bruk ser det slik ut:

 1 (ns demo (:use clojure.contrib.strint))
 2 
 3 (let [given "James" surname "Bond"]
 4   (println (<< "My name is ~{surname}, ~{given} ~{surname}.")))
 5 
 6 ; => My name is Bond, James Bond.
 7 
 8 (let [numbers '(1 2 5 9 4 4)]
 9   (println (<< "The sum of ~{numbers} is ~(apply + numbers).")))
10 
11 ; => The sum of (1 2 5 9 4 4) is 25.

Om du trodde regex var uforståelig…

De mest avanserte formateringsmulighetene jeg har sett er dem man har i Common Lisp. Format-strengene kan inneholdet sitt helt eget mini-språk, og hvis du synes at regex er komplisert og uforståelig så vent bare til du ser dette. Formatene kan for eksempel ha kontrollflyt, løkker og rekursjon!

Igjen har clojure en implementasjon av det samme, og den heter cl-format. Dette er bare en liten smakebit..

10 (ns demo (:use clojure.contrib.pprint))
11 
12 (defn print-dogs [n]
13   (cl-format true "~R dog~:[s are~; is~] here." n (= 1 n)))
14 
15 (print-dogs 1) ; => one dog is here.
16 (print-dogs 3) ; => three dogs are here.
17 
18 (cl-format true "Par:~{ <~S,~S>~}" '(A 1 B 2 C 3))
19 
20 ; => Par: <A,1> <B,2> <C,3>

Vil du vite mer om mulighetene kan du ta en titt i Common Lisp HyperSpec (som er den styggeste API-dokumentasjonen jeg noen sinne har sett).

Oppsummering

Den siste tiden, spesielt gjennom å studere Clojure, har jeg liksom gjennoppdaget printf. Når jeg husker på å benytte meg av de kraftige formateringsmulighetene resulterer det ofte i mer elegant kode. Konkatinering er clunky, bruk format!


comments powered by Disqus