struct Delim{T} <: Dates.AbstractDateToken d::T end charactercode(::Dates.DatePart{c}) where {c} = c function Format(f::AbstractString, locale::Dates.DateLocale=Dates.ENGLISH) tokens = Dates.AbstractDateToken[] prev = () prev_offset = 1 letters = String(collect(keys(Dates.CONVERSION_SPECIFIERS))) for m in eachmatch(Regex("(? s"\1") if !isempty(prev) letter, width = prev typ = Dates.CONVERSION_SPECIFIERS[letter] push!(tokens, Dates.DatePart{letter}(width, isempty(tran))) end if !isempty(tran) push!(tokens, length(tran) == 1 ? Delim(first(tran)) : Delim(tran)) end letter = f[m.offset] width = length(m.match) prev = (letter, width) prev_offset = m.offset + width end tran = replace(f[prev_offset:lastindex(f)], r"\\(.)" => s"\1") if !isempty(prev) letter, width = prev typ = Dates.CONVERSION_SPECIFIERS[letter] push!(tokens, Dates.DatePart{letter}(width, false)) end if !isempty(tran) push!(tokens, length(tran) == 1 ? Delim(first(tran)) : Delim(tran)) end return Format(tokens, locale) end function Format(f::AbstractString, locale::AbstractString) Format(f, Dates.LOCALES[locale]) end function Format(df::Dates.DateFormat{S, T}) where {S, T} N = fieldcount(T) dftokens = df.tokens tokens = Vector{Dates.AbstractDateToken}(undef, fieldcount(T)) Base.@nexprs 15 i -> begin if i <= N @inbounds tok = dftokens[i] @inbounds tokens[i] = tok isa Dates.Delim ? Delim(tok.d) : dftokens[i] end end if N > 15 for i = 16:N @inbounds tok = dftokens[i] @inbounds tokens[i] = tok isa Dates.Delim ? Delim(tok.d) : dftokens[i] end end return Format(tokens, df.locale) end function Base.show(io::IO, df::Format) print(io, "Parsers.dateformat\"") for t in df.tokens _show_content(io, t) end print(io, '"') end Base.Broadcast.broadcastable(x::Format) = Ref(x) function _show_content(io::IO, d::Dates.DatePart{c}) where c for i = 1:d.width print(io, c) end end function _show_content(io::IO, d::Delim{<:AbstractChar}) if d.d in keys(Dates.CONVERSION_SPECIFIERS) for i = 1:1 print(io, '\\', d.d) end else for i = 1:1 print(io, d.d) end end end function _show_content(io::IO, d::Delim) for c in d.d if c in keys(Dates.CONVERSION_SPECIFIERS) print(io, '\\') end print(io, c) end end function Base.show(io::IO, d::Delim) print(io, "Delim(") _show_content(io, d) print(io, ")") end macro dateformat_str(str) Format(str) end # Standard formats const ISODateTimeFormat = Format("yyyy-mm-dd\\THH:MM:SS.s") const ISODateFormat = Format("yyyy-mm-dd") const ISOTimeFormat = Format("HH:MM:SS.s") const RFC1123Format = Format("e, dd u yyyy HH:MM:SS") default_format(::Type{DateTime}) = ISODateTimeFormat default_format(::Type{Date}) = ISODateFormat default_format(::Type{Time}) = ISOTimeFormat maxdigits(d::Dates.DatePart) = d.fixed ? d.width : typemax(Int64) for c in "yYmdHIMS" @eval begin @inline function tryparsenext(d::Dates.DatePart{$c}, source, pos, len, b, code) return tryparsenext_base10(source, pos, len, b, code, maxdigits(d)) end end end @inline function tryparsenext_base10(source, pos, len, b, code, maxdigits) x::Int64 = 0 b -= UInt8('0') if b > 0x09 # character isn't a digit, INVALID value code |= INVALID b += UInt8('0') @goto done end ndigits = 0 @inbounds while true x = Int64(10) * x + Int64(b) ndigits += 1 pos += 1 incr!(source) if eof(source, pos, len) code |= EOF @goto done end b = peekbyte(source, pos) - UInt8('0') if b > 0x09 || ndigits == maxdigits b += UInt8('0') @goto done end end @label done return x, pos, b, code end ascii_lc(c::UInt8) = c in UInt8('A'):UInt8('Z') ? c + 0x20 : c function tryparsenext(d::Dates.DatePart{'p'}, source, pos, len, b, code) ap = ascii_lc(b) pos += 1 incr!(source) if !(ap == UInt8('a') || ap == UInt8('p')) || eof(source, pos, len) code |= INVALID_TOKEN else b = peekbyte(source, pos) if ascii_lc(b) != UInt8('m') code |= INVALID_TOKEN end pos += 1 incr!(source) if eof(source, pos, len) code |= EOF else b = peekbyte(source, pos) end end return ap == UInt8('a') ? Dates.AM : Dates.PM, pos, b, code end function nextchar(source, pos, len, b) u = UInt32(b) << 24 if !Base.between(b, 0x80, 0xf7) pos += 1 incr!(source) return reinterpret(Char, u), pos end return nextchar_continued(source, pos, len, u) end function nextchar_continued(source, pos, len, u) u < 0xc0000000 && (pos += 1; incr!(source); @goto ret) # first continuation byte pos += 1 incr!(source) eof(source, pos, len) && @goto ret b = peekbyte(source, pos) b & 0xc0 == 0x80 || @goto ret u |= UInt32(b) << 16 # second continuation byte pos += 1 incr!(source) (eof(source, pos, len)) | (u < 0xe0000000) && @goto ret b = peekbyte(source, pos) b & 0xc0 == 0x80 || @goto ret u |= UInt32(b) << 8 # third continuation byte pos += 1 incr!(source) (eof(source, pos, len)) | (u < 0xf0000000) && @goto ret b = peekbyte(source, pos) b & 0xc0 == 0x80 || @goto ret u |= UInt32(b); pos += 1; incr!(source) @label ret return reinterpret(Char, u), pos end for (tok, fn) in zip("uUeE", Any[Dates.monthabbr_to_value, Dates.monthname_to_value, Dates.dayabbr_to_value, Dates.dayname_to_value]) @eval function tryparsenext(d::Dates.DatePart{$tok}, source, pos, len, b, code, locale) startpos = pos while true c, pos = nextchar(source, pos, len, b) if !isletter(c) || eof(source, pos, len) pos -= 1 break end b = peekbyte(source, pos) end val = 0 if startpos == pos code |= INVALID_TOKEN else if source isa AbstractVector{UInt8} word = unsafe_string(pointer(source, startpos), pos - startpos) else # source isa IO fastseek!(source, startpos - 1) word = String(read(source, pos - startpos)) end val = $fn(word, locale) if val == 0 code |= INVALID_TOKEN end end return Int64(val), pos, b, code end end @inline function tryparsenext(d::Dates.DatePart{'s'}, source, pos, len, b, code, options) ms0, newpos, b, code = tryparsenext_base10(source, pos, len, b, code, maxdigits(d)) invalid(code) && return ms0, newpos, b, code rounding = options.rounding len = newpos - pos if len > 3 if rounding === nothing ms, r = divrem(ms0, Int64(10) ^ (len - 3)) if r != 0 code |= INEXACT end elseif rounding === RoundNearest ms = div(ms0, Int64(10) ^ (len - 3), RoundNearest) elseif rounding === RoundToZero ms = div(ms0, Int64(10) ^ (len - 3), RoundToZero) else ms = div(ms0, Int64(10) ^ (len - 3), rounding::RoundingMode) end else ms = ms0 * Int64(10) ^ (3 - len) end return ms, newpos, b, code end @inline function tryparsenext(d::Delim{<:AbstractChar}, source, pos, len, b, code) u = bswap(reinterpret(UInt32, d.d)) while true if b != (u & 0x000000ff) code |= INVALID_TOKEN break end u >>= 8 pos += 1 incr!(source) if eof(source, pos, len) code |= EOF | (u == UInt32(0) ? SUCCESS : INVALID_TOKEN) break end b = peekbyte(source, pos) u == UInt32(0) && break end return pos, b, code end function tryparsenext(d::Delim{String}, source, pos, len, b, code) bytes = codeunits(d.d) tlen = length(bytes) for dpos = 1:tlen @inbounds c = bytes[dpos] if b != c code |= INVALID_TOKEN break end pos += 1 incr!(source) if eof(source, pos, len) code |= EOF | (dpos == tlen ? SUCCESS : INVALID_TOKEN) break end b = peekbyte(source, pos) end return pos, b, code end # fallback that would call custom DatePart overloads that are expecting a string function tryparsenext(tok, source, pos, len, b, code)::Tuple{Any, Int, UInt8, ReturnCode} strlen = min(len - pos + 1, 64) str = getstring(source, PosLen(pos, strlen), 0x00) res = Dates.tryparsenext(tok, str, 1, strlen) if res === nothing val = nothing code |= INVALID_TOKEN else val, i = res pos += i - 1 if eof(source, pos, len) code |= EOF else b = peekbyte(source, pos) end end return val, pos, b, code end @inline function typeparser(::AbstractConf{T}, source::Union{AbstractVector{UInt8}, IO}, pos, len, b, code, pl, options) where {T <: Dates.TimeType} fmt = options.dateformat df = fmt === nothing ? default_format(T) : fmt tokens = df.tokens locale::Dates.DateLocale = df.locale year = month = day = Int64(1) hour = minute = second = millisecond = Int64(0) tz = "" ampm = Dates.TWENTYFOURHOUR extras = nothing for tok in tokens # @show pos, Char(b), code, typeof(tok) eof(code) && break if tok isa Delim{Char} pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif tok isa Delim{String} pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif T !== Time && tok isa Dates.DatePart{'y'} year, pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif T !== Time && tok isa Dates.DatePart{'Y'} year, pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif T !== Time && tok isa Dates.DatePart{'m'} month, pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif T !== Time && tok isa Dates.DatePart{'u'} month, pos, b, code = tryparsenext(tok, source, pos, len, b, code, locale) elseif T !== Time && tok isa Dates.DatePart{'U'} month, pos, b, code = tryparsenext(tok, source, pos, len, b, code, locale) elseif T !== Time && tok isa Dates.DatePart{'d'} day, pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif T !== Time && tok isa Dates.DatePart{'e'} _, pos, b, code = tryparsenext(tok, source, pos, len, b, code, locale) elseif T !== Time && tok isa Dates.DatePart{'E'} _, pos, b, code = tryparsenext(tok, source, pos, len, b, code, locale) elseif T !== Date && tok isa Dates.DatePart{'H'} hour, pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif T !== Date && tok isa Dates.DatePart{'I'} hour, pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif T !== Date && tok isa Dates.DatePart{'M'} minute, pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif T !== Date && tok isa Dates.DatePart{'S'} second, pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif T !== Date && tok isa Dates.DatePart{'p'} ampm, pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif T !== Date && tok isa Dates.DatePart{'s'} millisecond, pos, b, code = tryparsenext(tok, source, pos, len, b, code, options) elseif tok isa Dates.DatePart{'z'} tz, pos, b, code = tryparsenext(tok, source, pos, len, b, code) elseif tok isa Dates.DatePart{'Z'} tz, pos, b, code = tryparsenext(tok, source, pos, len, b, code) else # non-Dates defined character code # allocate extras if not already and parse if extras === nothing extras = IdDict{Type, Any}() end extraval, pos, b, code = tryparsenext(tok, source, pos, len, b, code)::Tuple{Any, Int, UInt8, ReturnCode} extras[Dates.CONVERSION_SPECIFIERS[charactercode(tok)]] = extraval end if invalid(code) if invalidtoken(code) code &= ~INVALID_TOKEN end break end # @show pos, Char(b), code end if T === Time @static if VERSION >= v"1.3-DEV" valid = Dates.validargs(T, hour, minute, second, millisecond, Int64(0), Int64(0), ampm) else valid = Dates.validargs(T, hour, minute, second, millisecond, Int64(0), Int64(0)) end elseif T === Date valid = Dates.validargs(T, year, month, day) elseif T === DateTime @static if VERSION >= v"1.3-DEV" valid = Dates.validargs(T, year, month, day, hour, minute, second, millisecond, ampm) else valid = Dates.validargs(T, year, month, day, hour, minute, second, millisecond) end elseif T.name.name === :ZonedDateTime valid = Dates.validargs(T, year, month, day, hour, minute, second, millisecond, tz) else # custom TimeType if extras === nothing extras = IdDict{Type, Any}() end extras[Year] = year; extras[Month] = month; extras[Day] = day; extras[Hour] = hour; extras[Minute] = minute; extras[Second] = second; extras[Millisecond] = millisecond; types = Dates.CONVERSION_TRANSLATIONS[T] vals = Vector{Any}(undef, length(types)) for (i, type) in enumerate(types) vals[i] = get(extras, type) do Dates.CONVERSION_DEFAULTS[type] end end valid = Dates.validargs(T, vals...) end if invalid(code) || valid !== nothing if T.name.name === :ZonedDateTime x = T(0, TimeZone("UTC")) else x = T(0) end code |= INVALID else if T === Time @static if VERSION >= v"1.3-DEV" x = Time(Nanosecond(1000000 * millisecond + 1000000000 * second + 60000000000 * minute + 3600000000000 * (Dates.adjusthour(hour, ampm)))) else x = Time(Nanosecond(1000000 * millisecond + 1000000000 * second + 60000000000 * minute + 3600000000000 * hour)) end elseif T === Date x = Date(Dates.UTD(Dates.totaldays(year, month, day))) elseif T === DateTime @static if VERSION >= v"1.3-DEV" x = DateTime(Dates.UTM(millisecond + 1000 * (second + 60 * minute + 3600 * (Dates.adjusthour(hour, ampm)) + 86400 * Dates.totaldays(year, month, day)))) else x = DateTime(Dates.UTM(millisecond + 1000 * (second + 60 * minute + 3600 * hour + 86400 * Dates.totaldays(year, month, day)))) end elseif T.name.name === :ZonedDateTime x = T(year, month, day, hour, minute, second, millisecond, tz) else # custom TimeType x = T(vals...) end code |= OK end if eof(source, pos, len) code |= EOF end return pos, code, PosLen(pl.pos, pos - pl.pos), x end