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 @@
- (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)