Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support AM/PM in date parsing/printing #32308

Merged
merged 11 commits into from
Jun 16, 2019
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
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Standard library changes

#### Dates

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

#### Sockets
Expand Down
36 changes: 31 additions & 5 deletions stdlib/Dates/src/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,24 @@ end

### Parse tokens

for c in "yYmdHMS"
for c in "yYmdHIMS"
@eval begin
@inline function tryparsenext(d::DatePart{$c}, str, i, len)
return tryparsenext_base10(str, i, len, min_width(d), max_width(d))
end
end
end

function tryparsenext(d::DatePart{'p'}, str, i, len)
i+1 > len && return nothing
c, ii = iterate(str, i)::Tuple{Char, Int}
ap = lowercase(c)
(ap == 'a' || ap == 'p') || return nothing
c, ii = iterate(str, ii)::Tuple{Char, Int}
lowercase(c) == 'm' || return nothing
return ap == 'a' ? AM : PM, ii
end

for (tok, fn) in zip("uUeE", [monthabbr_to_value, monthname_to_value, dayabbr_to_value, dayname_to_value])
@eval @inline function tryparsenext(d::DatePart{$tok}, str, i, len, locale)
next = tryparsenext_word(str, i, len, locale, max_width(d))
Expand Down Expand Up @@ -149,7 +159,9 @@ end

### Format tokens

for (c, fn) in zip("YmdHMS", [year, month, day, hour, minute, second])
hour12(dt) = let h = hour(dt); h > 12 ? h - 12 : h == 0 ? 12 : h; end

for (c, fn) in zip("YmdHIMS", [year, month, day, hour, hour12, minute, second])
@eval function format(io, d::DatePart{$c}, dt)
print(io, string($fn(dt), base = 10, pad = d.width))
end
Expand All @@ -161,6 +173,11 @@ for (tok, fn) in zip("uU", [monthabbr, monthname])
end
end

function format(io, d::DatePart{'p'}, dt, locale)
ampm = hour(dt) < 12 ? "AM" : "PM" # fixme: locale-specific?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell from some googling, there are only a handful of english-speaking countries that use AM/PM, so yeah, I'm not quite sure how critical it is to support locale here. For reference (https://www.timeanddate.com/time/am-and-pm.html)

print(io, ampm)
end

for (tok, fn) in zip("eE", [dayabbr, dayname])
@eval function format(io, ::DatePart{$tok}, dt, locale)
print(io, $fn(dayofweek(dt), locale))
Expand Down Expand Up @@ -283,9 +300,11 @@ const CONVERSION_SPECIFIERS = Dict{Char, Type}(
'E' => DayOfWeekToken,
'd' => Day,
'H' => Hour,
'I' => Hour,
'M' => Minute,
'S' => Second,
's' => Millisecond,
'p' => AMPM,
)

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

# Specifies the required fields in order to parse a TimeType
# Note: Allows for addition of new TimeTypes
const CONVERSION_TRANSLATIONS = IdDict{Type, Any}(
Date => (Year, Month, Day),
DateTime => (Year, Month, Day, Hour, Minute, Second, Millisecond),
Time => (Hour, Minute, Second, Millisecond, Microsecond, Nanosecond),
DateTime => (Year, Month, Day, Hour, Minute, Second, Millisecond, AMPM),
Time => (Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, AMPM),
)

"""
Expand All @@ -327,19 +347,25 @@ string:
| `u` | Jan | Matches abbreviated months according to the `locale` keyword |
| `U` | January | Matches full month names according to the `locale` keyword |
| `d` | 1, 01 | Matches 1 or 2-digit days |
| `H` | 00 | Matches hours |
| `H` | 00 | Matches hours (24-hour clock) |
| `I` | 00 | For outputting hours with 12-hour clock |
| `M` | 00 | Matches minutes |
| `S` | 00 | Matches seconds |
| `s` | .500 | Matches milliseconds |
| `e` | Mon, Tues | Matches abbreviated days of the week |
| `E` | Monday | Matches full name days of the week |
| `p` | AM | Matches AM/PM (case-insensitive) |
| `yyyymmdd` | 19960101 | Matches fixed-width year, month, and day |

Characters not listed above are normally treated as delimiters between date and time slots.
For example a `dt` string of "1996-01-15T00:00:00.0" would have a `format` string like
"y-m-dTH:M:S.s". If you need to use a code character as a delimiter you can escape it using
backslash. The date "1995y01m" would have the format "y\\ym\\m".

Note that 12:00AM corresponds 00:00 (midnight), and 12:00PM corresponds to 12:00 (noon).
When parsing a time with a `p` specifier, any hour (either `H` or `I`) is interpreted as
as a 12-hour clock, so the `I` code is mainly useful for output.

Creating a DateFormat object is expensive. Whenever possible, create it once and use it many times
or try the `dateformat""` string macro. Using this macro creates the DateFormat object once at
macro expansion time and reuses it later. see [`@dateformat_str`](@ref).
Expand Down
41 changes: 30 additions & 11 deletions stdlib/Dates/src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ or [`nothing`](@ref) if no message is provided. For use by `validargs`.
argerror(msg::String) = ArgumentError(msg)
argerror() = nothing

# Julia uses 24-hour clocks internally, but user input can be AM/PM with 12pm == noon and 12am == midnight.
@enum AMPM AM PM TWENTYFOURHOUR
function adjusthour(h::Int64, ampm::AMPM)
ampm == TWENTYFOURHOUR && return h
ampm == PM && h < 12 && return h + 12
ampm == AM && h == 12 && return Int64(0)
return h
end

### CONSTRUCTORS ###
# Core constructors
"""
Expand All @@ -178,19 +187,24 @@ argerror() = nothing
Construct a `DateTime` type by parts. Arguments must be convertible to [`Int64`](@ref).
"""
function DateTime(y::Int64, m::Int64=1, d::Int64=1,
h::Int64=0, mi::Int64=0, s::Int64=0, ms::Int64=0)
err = validargs(DateTime, y, m, d, h, mi, s, ms)
h::Int64=0, mi::Int64=0, s::Int64=0, ms::Int64=0, ampm::AMPM=TWENTYFOURHOUR)
err = validargs(DateTime, y, m, d, h, mi, s, ms, ampm)
err === nothing || throw(err)
h = adjusthour(h, ampm)
rata = ms + 1000 * (s + 60mi + 3600h + 86400 * totaldays(y, m, d))
return DateTime(UTM(rata))
end

function validargs(::Type{DateTime}, y::Int64, m::Int64, d::Int64,
h::Int64, mi::Int64, s::Int64, ms::Int64)
h::Int64, mi::Int64, s::Int64, ms::Int64, ampm::AMPM=TWENTYFOURHOUR)
0 < m < 13 || return argerror("Month: $m out of range (1:12)")
0 < d < daysinmonth(y, m) + 1 || return argerror("Day: $d out of range (1:$(daysinmonth(y, m)))")
-1 < h < 24 || (h == 24 && mi==s==ms==0) ||
return argerror("Hour: $h out of range (0:23)")
if ampm == TWENTYFOURHOUR # 24-hour clock
-1 < h < 24 || (h == 24 && mi==s==ms==0) ||
return argerror("Hour: $h out of range (0:23)")
else
0 < h < 13 || return argerror("Hour: $h out of range (1:12)")
end
-1 < mi < 60 || return argerror("Minute: $mi out of range (0:59)")
-1 < s < 60 || return argerror("Second: $s out of range (0:59)")
-1 < ms < 1000 || return argerror("Millisecond: $ms out of range (0:999)")
Expand Down Expand Up @@ -223,14 +237,19 @@ Date(dt::Base.Libc.TmStruct) = Date(1900 + dt.year, 1 + dt.month, dt.mday)

Construct a `Time` type by parts. Arguments must be convertible to [`Int64`](@ref).
"""
function Time(h::Int64, mi::Int64=0, s::Int64=0, ms::Int64=0, us::Int64=0, ns::Int64=0)
err = validargs(Time, h, mi, s, ms, us, ns)
function Time(h::Int64, mi::Int64=0, s::Int64=0, ms::Int64=0, us::Int64=0, ns::Int64=0, ampm::AMPM=TWENTYFOURHOUR)
err = validargs(Time, h, mi, s, ms, us, ns, ampm)
err === nothing || throw(err)
h = adjusthour(h, ampm)
return Time(Nanosecond(ns + 1000us + 1000000ms + 1000000000s + 60000000000mi + 3600000000000h))
end

function validargs(::Type{Time}, h::Int64, mi::Int64, s::Int64, ms::Int64, us::Int64, ns::Int64)
-1 < h < 24 || return argerror("Hour: $h out of range (0:23)")
function validargs(::Type{Time}, h::Int64, mi::Int64, s::Int64, ms::Int64, us::Int64, ns::Int64, ampm::AMPM=TWENTYFOURHOUR)
if ampm == TWENTYFOURHOUR # 24-hour clock
-1 < h < 24 || return argerror("Hour: $h out of range (0:23)")
else
0 < h < 13 || return argerror("Hour: $h out of range (1:12)")
end
-1 < mi < 60 || return argerror("Minute: $mi out of range (0:59)")
-1 < s < 60 || return argerror("Second: $s out of range (0:59)")
-1 < ms < 1000 || return argerror("Millisecond: $ms out of range (0:999)")
Expand Down Expand Up @@ -345,9 +364,9 @@ function DateTime(dt::Date, t::Time)
end

# Fallback constructors
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))
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)
Date(y, m=1, d=1) = Date(Int64(y), Int64(m), Int64(d))
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))
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)

# Traits, Equality
Base.isfinite(::Union{Type{T}, T}) where {T<:TimeType} = true
Expand Down
22 changes: 22 additions & 0 deletions stdlib/Dates/test/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -566,4 +566,26 @@ end
@test_throws ArgumentError DateTime(2018, 1, 1, 24, 0, 0, 1)
end

@testset "AM/PM" begin
for (t12,t24) in (("12:00am","00:00"), ("12:07am","00:07"), ("01:24AM","01:24"),
("12:00pm","12:00"), ("12:15pm","12:15"), ("11:59PM","23:59"))
d = DateTime("2018-01-01T$t24:00")
t = Time("$t24:00")
for HH in ("HH","II")
@test DateTime("2018-01-01 $t12","yyyy-mm-dd $HH:MMp") == d
@test Time("$t12","$HH:MMp") == t
end
tmstruct = Libc.strptime("%I:%M%p", t12)
@test Time(tmstruct) == t
@test uppercase(t12) == Dates.format(t, "II:MMp") ==
Dates.format(d, "II:MMp") ==
Libc.strftime("%I:%M%p", tmstruct)
end
for bad in ("00:24am", "00:24pm", "13:24pm", "2pm", "12:24p.m.", "12:24 pm", "12:24pµ")
@eval @test_throws ArgumentError Time($bad, "II:MMp")
end
# if am/pm is missing, defaults to 24-hour clock
@eval Time("13:24", "II:MMp") == Time("13:24", "HH:MM")
end

end