diff --git a/docs/Datetime.html b/docs/Datetime.html index 043cf1f..9c9de78 100644 --- a/docs/Datetime.html +++ b/docs/Datetime.html @@ -1108,7 +1108,7 @@

defn

- (Fn [a, (Ref String b)] String) + (Fn [(Ref Datetime a), (Ref String b)] String)

                         (strftime dt s)
@@ -1116,6 +1116,9 @@ 

formats the Datetime dt according to the string provided in s. The formatting options mimic the C interface.

+

Text outside a directive is copied verbatim. %% renders a single %, as does +a trailing % with nothing after it; an unrecognized specifier renders as the +character that follows the %.

You can alternatively use the fmt macro or format function, but you’ll only be able to use one of the formatting properties there. Thus this function is provided as a convenience for fine-grained string tweaking.

diff --git a/test/time.carp b/test/time.carp index 7446600..498afeb 100644 --- a/test/time.carp +++ b/test/time.carp @@ -998,6 +998,76 @@ &@"Mar" &(Datetime.strftime &(Datetime.date 2024 3 15) "%h") "strftime %h aliases the abbreviated month name") + + (assert-equal test + &@"Date: 2024" + &(Datetime.strftime &(Datetime.date 2024 3 15) "Date: %Y") + "strftime keeps a leading literal") + (assert-equal test + &@"Day 15, ok" + &(Datetime.strftime &(Datetime.date 2024 3 15) "Day %d, ok") + "strftime keeps a leading literal starting with a specifier letter") + (assert-equal test + &@"zzz 2024" + &(Datetime.strftime &(Datetime.date 2024 3 15) "zzz %Y") + "strftime keeps a leading literal starting with a tz specifier letter") + (assert-equal test + &@"hello" + &(Datetime.strftime &(Datetime.date 2024 3 15) "hello") + "strftime passes through a format with no directive") + (assert-equal test + &@"" + &(Datetime.strftime &(Datetime.date 2024 3 15) "") + "strftime renders the empty format as the empty string") + (assert-equal test + &@"100% done 2024" + &(Datetime.strftime &(Datetime.date 2024 3 15) "100%% done %Y") + "strftime renders %% as a literal percent") + (assert-equal test + &@"%Y" + &(Datetime.strftime &(Datetime.date 2024 3 15) "%%Y") + "strftime does not expand the letter after an escaped percent") + (assert-equal test + &@"%2024" + &(Datetime.strftime &(Datetime.date 2024 3 15) "%%%Y") + "strftime resumes directives after an escaped percent") + (assert-equal test + &@"Q" + &(Datetime.strftime &(Datetime.date 2024 3 15) "%Q") + "strftime renders an unknown specifier as the character itself") + (assert-equal test + &[195b 164b] + &(String.to-bytes &(Datetime.strftime &(Datetime.date 2024 3 15) "%ä")) + "strftime renders an unknown non-ASCII specifier as that character") + (assert-equal test + &[226b 130b 172b 120b] + &(String.to-bytes &(Datetime.strftime &(Datetime.date 2024 3 15) "%€x")) + "strftime reassembles a multi-byte unknown specifier before a literal") + (assert-equal test + &@"2024%" + &(Datetime.strftime &(Datetime.date 2024 3 15) "%Y%") + "strftime renders a trailing lone percent as a literal percent") + (assert-equal test + &@"%" + &(Datetime.strftime &(Datetime.date 2024 3 15) "%") + "strftime renders a lone percent as a literal percent") + (assert-equal test + &@"Größe: 2024 μs" + &(Datetime.strftime &(Datetime.date 2024 3 15) "Größe: %Y μs") + "strftime copies non-ASCII literals verbatim") + (assert-equal test + &@"Größe: 2024" + &(Datetime.format "Größe: %Y" &(Datetime.date 2024 3 15)) + "format slices non-ASCII literals by byte") + (assert-equal test + &@"Größe: 2024" + &(fmt "Größe: %Y" &(Datetime.date 2024 3 15)) + "fmt reaches format with a non-ASCII literal") + (assert-equal test + &@"hello" + &(Datetime.format "hello" &(Datetime.date 2024 3 15)) + "format passes through a slice with no directive") + (assert-equal test &(Result.Success (Datetime.date 2024 3 15)) &(Datetime.strptime "2024-03-15 11" "%Y-%m-%d %V") diff --git a/time.carp b/time.carp index b79df5a..de06493 100644 --- a/time.carp +++ b/time.carp @@ -480,9 +480,15 @@ All of the time information is obtained from the operating system directly.") (register format (Fn [(Ref String) (Ref Datetime)] String)) (implements format Datetime.format) + (register format-for (Fn [Char (Ref Datetime)] String)) + (doc strftime "formats the `Datetime` `dt` according to the string provided in `s`. The formatting options mimic [the C interface](http://www.cplusplus.com/reference/ctime/strftime/). +Text outside a directive is copied verbatim. `%%` renders a single `%`, as does +a trailing `%` with nothing after it; an unrecognized specifier renders as the +character that follows the `%`. + You can alternatively use the `fmt` macro or `format` function, but you’ll only be able to use one of the formatting properties there. Thus this function is provided as a convenience for fine-grained string tweaking. @@ -493,18 +499,29 @@ Example: &(Datetime.strftime &(Datetime.now) \"%Y-%m-%d %I:%M:%S.%n %p %z\") ```") (defn strftime [dt s] - (let [strings (String.split-by s &[\%]) - ln (Int.dec (Array.length &strings)) - rng (Result.from-success (Array.range 0 ln 1) [])] - (String.concat - &(Array.zip - &(fn [x i] - (cond - (and (String.empty? x) (or (= i &0) (= i &ln))) @"" - (String.empty? x) @"%" - (format &(String.concat &[@"%" @x]) dt))) - &strings - &rng)))) + (let-do [result @"" + len (String.length s) + i 0 + lit 0] + (while (< i len) + (if (= \% (String.char-at s i)) + (do + (when (< lit i) + (set! result (String.concat &[result (String.byte-slice s lit i)]))) + (if (< (Int.inc i) len) + (let-do [spec (String.char-at s (Int.inc i))] + (set! result + (String.concat + &[result (if (= \% spec) @"%" (format-for spec dt))])) + (set! i (+ i 2))) + (do + (set! result (String.concat &[result @"%"])) + (set! i (Int.inc i)))) + (set! lit i)) + (set! i (Int.inc i)))) + (when (< lit len) + (set! result (String.concat &[result (String.byte-slice s lit len)]))) + result)) (private format-for) (hidden format-for) @@ -587,18 +604,20 @@ Example: (strftime dt "%I:%M:%S %p") \h (month-short-string dt) - (str c))) + (String.from-bytes &[(Byte.from-int (Char.to-int c))]))) (doc format "The interface implementation of `format` for `Datetime` values. It mimics [the C interface](http://www.cplusplus.com/reference/ctime/strftime/).") (defn format [s dt] (let [idx (String.index-of s \%) - formatter (String.char-at s (Int.inc idx))] - (String.concat - &[(String.prefix s idx) - (format-for formatter dt) - (String.suffix s (+ idx 2))]))) + len (String.length s)] + (if (or (< idx 0) (= idx (Int.dec len))) + @s + (String.concat + &[(String.byte-slice s 0 idx) + (format-for (String.char-at s (Int.inc idx)) dt) + (String.byte-slice s (+ idx 2) len)])))) (private parse-digits) (hidden parse-digits)