Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/Datetime.html
Original file line number Diff line number Diff line change
Expand Up @@ -1108,14 +1108,17 @@ <h3 id="strftime">
defn
</div>
<p class="sig">
(Fn [a, (Ref String b)] String)
(Fn [(Ref Datetime a), (Ref String b)] String)
</p>
<pre class="args">
(strftime dt s)
</pre>
<p class="doc">
<p>formats the <code>Datetime</code> <code>dt</code> according to the string provided
in <code>s</code>. The formatting options mimic <a href="http://www.cplusplus.com/reference/ctime/strftime/">the C interface</a>.</p>
<p>Text outside a directive is copied verbatim. <code>%%</code> renders a single <code>%</code>, as does
a trailing <code>%</code> with nothing after it; an unrecognized specifier renders as the
character that follows the <code>%</code>.</p>
<p>You can alternatively use the <code>fmt</code> macro or <code>format</code> 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.</p>
Expand Down
70 changes: 70 additions & 0 deletions test/time.carp
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
55 changes: 37 additions & 18 deletions time.carp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down