Skip to content

Commit 092d695

Browse files
authored
Merge pull request #32308 from JuliaLang/sgj/ampm
support AM/PM in date parsing/printing
2 parents 1eca37e + e192792 commit 092d695

File tree

4 files changed

+84
-16
lines changed

4 files changed

+84
-16
lines changed

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Standard library changes
5050

5151
#### Dates
5252

53+
* `DateTime` and `Time` formatting/parsing now supports 12-hour clocks with AM/PM via `I` and `p` codes, similar to `strftime` ([#32308]).
5354
* Fixed `repr` such that it displays `Time` as it would be entered in Julia ([#32103]).
5455

5556
#### Sockets

stdlib/Dates/src/io.jl

+31-5
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,24 @@ end
111111

112112
### Parse tokens
113113

114-
for c in "yYmdHMS"
114+
for c in "yYmdHIMS"
115115
@eval begin
116116
@inline function tryparsenext(d::DatePart{$c}, str, i, len)
117117
return tryparsenext_base10(str, i, len, min_width(d), max_width(d))
118118
end
119119
end
120120
end
121121

122+
function tryparsenext(d::DatePart{'p'}, str, i, len)
123+
i+1 > len && return nothing
124+
c, ii = iterate(str, i)::Tuple{Char, Int}
125+
ap = lowercase(c)
126+
(ap == 'a' || ap == 'p') || return nothing
127+
c, ii = iterate(str, ii)::Tuple{Char, Int}
128+
lowercase(c) == 'm' || return nothing
129+
return ap == 'a' ? AM : PM, ii
130+
end
131+
122132
for (tok, fn) in zip("uUeE", [monthabbr_to_value, monthname_to_value, dayabbr_to_value, dayname_to_value])
123133
@eval @inline function tryparsenext(d::DatePart{$tok}, str, i, len, locale)
124134
next = tryparsenext_word(str, i, len, locale, max_width(d))
@@ -149,7 +159,9 @@ end
149159

150160
### Format tokens
151161

152-
for (c, fn) in zip("YmdHMS", [year, month, day, hour, minute, second])
162+
hour12(dt) = let h = hour(dt); h > 12 ? h - 12 : h == 0 ? 12 : h; end
163+
164+
for (c, fn) in zip("YmdHIMS", [year, month, day, hour, hour12, minute, second])
153165
@eval function format(io, d::DatePart{$c}, dt)
154166
print(io, string($fn(dt), base = 10, pad = d.width))
155167
end
@@ -161,6 +173,11 @@ for (tok, fn) in zip("uU", [monthabbr, monthname])
161173
end
162174
end
163175

176+
function format(io, d::DatePart{'p'}, dt, locale)
177+
ampm = hour(dt) < 12 ? "AM" : "PM" # fixme: locale-specific?
178+
print(io, ampm)
179+
end
180+
164181
for (tok, fn) in zip("eE", [dayabbr, dayname])
165182
@eval function format(io, ::DatePart{$tok}, dt, locale)
166183
print(io, $fn(dayofweek(dt), locale))
@@ -283,9 +300,11 @@ const CONVERSION_SPECIFIERS = Dict{Char, Type}(
283300
'E' => DayOfWeekToken,
284301
'd' => Day,
285302
'H' => Hour,
303+
'I' => Hour,
286304
'M' => Minute,
287305
'S' => Second,
288306
's' => Millisecond,
307+
'p' => AMPM,
289308
)
290309

291310
# Default values are needed when a conversion specifier is used in a DateFormat for parsing
@@ -302,14 +321,15 @@ const CONVERSION_DEFAULTS = IdDict{Type, Any}(
302321
Millisecond => Int64(0),
303322
Microsecond => Int64(0),
304323
Nanosecond => Int64(0),
324+
AMPM => TWENTYFOURHOUR,
305325
)
306326

307327
# Specifies the required fields in order to parse a TimeType
308328
# Note: Allows for addition of new TimeTypes
309329
const CONVERSION_TRANSLATIONS = IdDict{Type, Any}(
310330
Date => (Year, Month, Day),
311-
DateTime => (Year, Month, Day, Hour, Minute, Second, Millisecond),
312-
Time => (Hour, Minute, Second, Millisecond, Microsecond, Nanosecond),
331+
DateTime => (Year, Month, Day, Hour, Minute, Second, Millisecond, AMPM),
332+
Time => (Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, AMPM),
313333
)
314334

315335
"""
@@ -327,19 +347,25 @@ string:
327347
| `u` | Jan | Matches abbreviated months according to the `locale` keyword |
328348
| `U` | January | Matches full month names according to the `locale` keyword |
329349
| `d` | 1, 01 | Matches 1 or 2-digit days |
330-
| `H` | 00 | Matches hours |
350+
| `H` | 00 | Matches hours (24-hour clock) |
351+
| `I` | 00 | For outputting hours with 12-hour clock |
331352
| `M` | 00 | Matches minutes |
332353
| `S` | 00 | Matches seconds |
333354
| `s` | .500 | Matches milliseconds |
334355
| `e` | Mon, Tues | Matches abbreviated days of the week |
335356
| `E` | Monday | Matches full name days of the week |
357+
| `p` | AM | Matches AM/PM (case-insensitive) |
336358
| `yyyymmdd` | 19960101 | Matches fixed-width year, month, and day |
337359
338360
Characters not listed above are normally treated as delimiters between date and time slots.
339361
For example a `dt` string of "1996-01-15T00:00:00.0" would have a `format` string like
340362
"y-m-dTH:M:S.s". If you need to use a code character as a delimiter you can escape it using
341363
backslash. The date "1995y01m" would have the format "y\\ym\\m".
342364
365+
Note that 12:00AM corresponds 00:00 (midnight), and 12:00PM corresponds to 12:00 (noon).
366+
When parsing a time with a `p` specifier, any hour (either `H` or `I`) is interpreted as
367+
as a 12-hour clock, so the `I` code is mainly useful for output.
368+
343369
Creating a DateFormat object is expensive. Whenever possible, create it once and use it many times
344370
or try the `dateformat""` string macro. Using this macro creates the DateFormat object once at
345371
macro expansion time and reuses it later. see [`@dateformat_str`](@ref).

stdlib/Dates/src/types.jl

+30-11
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,15 @@ or [`nothing`](@ref) if no message is provided. For use by `validargs`.
170170
argerror(msg::String) = ArgumentError(msg)
171171
argerror() = nothing
172172

173+
# Julia uses 24-hour clocks internally, but user input can be AM/PM with 12pm == noon and 12am == midnight.
174+
@enum AMPM AM PM TWENTYFOURHOUR
175+
function adjusthour(h::Int64, ampm::AMPM)
176+
ampm == TWENTYFOURHOUR && return h
177+
ampm == PM && h < 12 && return h + 12
178+
ampm == AM && h == 12 && return Int64(0)
179+
return h
180+
end
181+
173182
### CONSTRUCTORS ###
174183
# Core constructors
175184
"""
@@ -178,19 +187,24 @@ argerror() = nothing
178187
Construct a `DateTime` type by parts. Arguments must be convertible to [`Int64`](@ref).
179188
"""
180189
function DateTime(y::Int64, m::Int64=1, d::Int64=1,
181-
h::Int64=0, mi::Int64=0, s::Int64=0, ms::Int64=0)
182-
err = validargs(DateTime, y, m, d, h, mi, s, ms)
190+
h::Int64=0, mi::Int64=0, s::Int64=0, ms::Int64=0, ampm::AMPM=TWENTYFOURHOUR)
191+
err = validargs(DateTime, y, m, d, h, mi, s, ms, ampm)
183192
err === nothing || throw(err)
193+
h = adjusthour(h, ampm)
184194
rata = ms + 1000 * (s + 60mi + 3600h + 86400 * totaldays(y, m, d))
185195
return DateTime(UTM(rata))
186196
end
187197

188198
function validargs(::Type{DateTime}, y::Int64, m::Int64, d::Int64,
189-
h::Int64, mi::Int64, s::Int64, ms::Int64)
199+
h::Int64, mi::Int64, s::Int64, ms::Int64, ampm::AMPM=TWENTYFOURHOUR)
190200
0 < m < 13 || return argerror("Month: $m out of range (1:12)")
191201
0 < d < daysinmonth(y, m) + 1 || return argerror("Day: $d out of range (1:$(daysinmonth(y, m)))")
192-
-1 < h < 24 || (h == 24 && mi==s==ms==0) ||
193-
return argerror("Hour: $h out of range (0:23)")
202+
if ampm == TWENTYFOURHOUR # 24-hour clock
203+
-1 < h < 24 || (h == 24 && mi==s==ms==0) ||
204+
return argerror("Hour: $h out of range (0:23)")
205+
else
206+
0 < h < 13 || return argerror("Hour: $h out of range (1:12)")
207+
end
194208
-1 < mi < 60 || return argerror("Minute: $mi out of range (0:59)")
195209
-1 < s < 60 || return argerror("Second: $s out of range (0:59)")
196210
-1 < ms < 1000 || return argerror("Millisecond: $ms out of range (0:999)")
@@ -223,14 +237,19 @@ Date(dt::Base.Libc.TmStruct) = Date(1900 + dt.year, 1 + dt.month, dt.mday)
223237
224238
Construct a `Time` type by parts. Arguments must be convertible to [`Int64`](@ref).
225239
"""
226-
function Time(h::Int64, mi::Int64=0, s::Int64=0, ms::Int64=0, us::Int64=0, ns::Int64=0)
227-
err = validargs(Time, h, mi, s, ms, us, ns)
240+
function Time(h::Int64, mi::Int64=0, s::Int64=0, ms::Int64=0, us::Int64=0, ns::Int64=0, ampm::AMPM=TWENTYFOURHOUR)
241+
err = validargs(Time, h, mi, s, ms, us, ns, ampm)
228242
err === nothing || throw(err)
243+
h = adjusthour(h, ampm)
229244
return Time(Nanosecond(ns + 1000us + 1000000ms + 1000000000s + 60000000000mi + 3600000000000h))
230245
end
231246

232-
function validargs(::Type{Time}, h::Int64, mi::Int64, s::Int64, ms::Int64, us::Int64, ns::Int64)
233-
-1 < h < 24 || return argerror("Hour: $h out of range (0:23)")
247+
function validargs(::Type{Time}, h::Int64, mi::Int64, s::Int64, ms::Int64, us::Int64, ns::Int64, ampm::AMPM=TWENTYFOURHOUR)
248+
if ampm == TWENTYFOURHOUR # 24-hour clock
249+
-1 < h < 24 || return argerror("Hour: $h out of range (0:23)")
250+
else
251+
0 < h < 13 || return argerror("Hour: $h out of range (1:12)")
252+
end
234253
-1 < mi < 60 || return argerror("Minute: $mi out of range (0:59)")
235254
-1 < s < 60 || return argerror("Second: $s out of range (0:59)")
236255
-1 < ms < 1000 || return argerror("Millisecond: $ms out of range (0:999)")
@@ -345,9 +364,9 @@ function DateTime(dt::Date, t::Time)
345364
end
346365

347366
# Fallback constructors
348-
DateTime(y, m=1, d=1, h=0, mi=0, s=0, ms=0) = DateTime(Int64(y), Int64(m), Int64(d), Int64(h), Int64(mi), Int64(s), Int64(ms))
367+
DateTime(y, m=1, d=1, h=0, mi=0, s=0, ms=0, ampm::AMPM=TWENTYFOURHOUR) = DateTime(Int64(y), Int64(m), Int64(d), Int64(h), Int64(mi), Int64(s), Int64(ms), ampm)
349368
Date(y, m=1, d=1) = Date(Int64(y), Int64(m), Int64(d))
350-
Time(h, mi=0, s=0, ms=0, us=0, ns=0) = Time(Int64(h), Int64(mi), Int64(s), Int64(ms), Int64(us), Int64(ns))
369+
Time(h, mi=0, s=0, ms=0, us=0, ns=0, ampm::AMPM=TWENTYFOURHOUR) = Time(Int64(h), Int64(mi), Int64(s), Int64(ms), Int64(us), Int64(ns), ampm)
351370

352371
# Traits, Equality
353372
Base.isfinite(::Union{Type{T}, T}) where {T<:TimeType} = true

stdlib/Dates/test/io.jl

+22
Original file line numberDiff line numberDiff line change
@@ -566,4 +566,26 @@ end
566566
@test_throws ArgumentError DateTime(2018, 1, 1, 24, 0, 0, 1)
567567
end
568568

569+
@testset "AM/PM" begin
570+
for (t12,t24) in (("12:00am","00:00"), ("12:07am","00:07"), ("01:24AM","01:24"),
571+
("12:00pm","12:00"), ("12:15pm","12:15"), ("11:59PM","23:59"))
572+
d = DateTime("2018-01-01T$t24:00")
573+
t = Time("$t24:00")
574+
for HH in ("HH","II")
575+
@test DateTime("2018-01-01 $t12","yyyy-mm-dd $HH:MMp") == d
576+
@test Time("$t12","$HH:MMp") == t
577+
end
578+
tmstruct = Libc.strptime("%I:%M%p", t12)
579+
@test Time(tmstruct) == t
580+
@test uppercase(t12) == Dates.format(t, "II:MMp") ==
581+
Dates.format(d, "II:MMp") ==
582+
Libc.strftime("%I:%M%p", tmstruct)
583+
end
584+
for bad in ("00:24am", "00:24pm", "13:24pm", "2pm", "12:24p.m.", "12:24 pm", "12:24pµ")
585+
@eval @test_throws ArgumentError Time($bad, "II:MMp")
586+
end
587+
# if am/pm is missing, defaults to 24-hour clock
588+
@eval Time("13:24", "II:MMp") == Time("13:24", "HH:MM")
589+
end
590+
569591
end

0 commit comments

Comments
 (0)