Skip to content
This repository has been archived by the owner on Dec 16, 2022. It is now read-only.

Commit

Permalink
Merge pull request JuliaLang#12799 from omus/dates_extensibility
Browse files Browse the repository at this point in the history
Made DateFormat Extensible
  • Loading branch information
quinnj committed Aug 28, 2015
2 parents 099dfd1 + 460cca8 commit c321bf8
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 69 deletions.
1 change: 1 addition & 0 deletions base/dates/adjusters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function lastdayofyear(dt::Date)
y,m,d = yearmonthday(dt)
return Date(UTD(value(dt)+daysinyear(y)-dayofyear(y,m,d)))
end
lastdayofyear(dt::DateTime) = DateTime(lastdayofyear(Date(dt)))

@vectorize_1arg TimeType firstdayofyear
@vectorize_1arg TimeType lastdayofyear
Expand Down
176 changes: 107 additions & 69 deletions base/dates/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,123 +34,161 @@ const MONTHTOVALUE = Dict{UTF8String,Dict{UTF8String,Int}}("english"=>english)
const MONTHTOVALUEABBR = Dict{UTF8String,Dict{UTF8String,Int}}("english"=>abbrenglish)

# Date/DateTime Parsing
abstract Slot{P<:AbstractTime}
abstract Slot{T<:Any}

immutable DelimitedSlot{P<:AbstractTime} <: Slot{P}
i::Int
period::Type{P}
immutable DelimitedSlot{T<:Any} <: Slot{T}
parser::Type{T}
letter::Char
width::Int
option::Int
locale::AbstractString
transition::Union{Regex,AbstractString}
end

immutable FixedWidthSlot{P<:AbstractTime} <: Slot{P}
i::Int
period::Type{P}
immutable FixedWidthSlot{T<:Any} <: Slot{T}
parser::Type{T}
letter::Char
width::Int
option::Int
locale::AbstractString
end

immutable DateFormat
slots::Array{Slot,1}
begtran # optional transition from the start of a string to the 1st slot
trans #trans[i] == how to transition FROM slots[i] TO slots[i+1]
prefix::AbstractString # optional transition from the start of a string to the 1st slot
locale::AbstractString
end

abstract DayOfWeekSlot

# Slot rules translate letters into types. Note that
# list of rules can be extended.
immutable SlotRule
rules::Array{Type}
end
const SLOT_RULE = SlotRule(Array{Type}(256))

immutable DayOfWeekSlot <: AbstractTime end
getindex(collection::SlotRule, key::Char) = collection.rules[Int(key)]
setindex!(collection::SlotRule, value::Type, key::Char) = collection.rules[Int(key)] = value
keys(c::SlotRule) = map(Char, filter(el -> isdefined(c.rules, el), eachindex(c.rules)))

duplicates(slots) = any(map(x->count(y->x.period==y.period,slots),slots) .> 1)
SLOT_RULE['y'] = Year
SLOT_RULE['m'] = Month
SLOT_RULE['u'] = Month
SLOT_RULE['U'] = Month
SLOT_RULE['e'] = DayOfWeekSlot
SLOT_RULE['E'] = DayOfWeekSlot
SLOT_RULE['d'] = Day
SLOT_RULE['H'] = Hour
SLOT_RULE['M'] = Minute
SLOT_RULE['S'] = Second
SLOT_RULE['s'] = Millisecond

duplicates(slots) = any(map(x->count(y->x.parser==y.parser,slots),slots) .> 1)

function DateFormat(f::AbstractString,locale::AbstractString="english")
slots = Slot[]
trans = []
begtran = match(r"^.*?(?=[ymuUdHMSsEe])",f).match
ss = split(f,r"^.*?(?=[ymuUdHMSsEe])")
s = split(begtran == "" ? ss[1] : ss[2],r"[^ymuUdHMSsEe]+|(?<=([ymuUdHMSsEe])(?!\1))")
for (i,k) in enumerate(s)
k == "" && break
tran = i >= endof(s) ? r"$" : match(Regex("(?<=$(s[i])).*(?=$(s[i+1]))"),f).match
slot = tran == "" ? FixedWidthSlot : DelimitedSlot
width = length(k)
typ = 'E' in k ? DayOfWeekSlot : 'e' in k ? DayOfWeekSlot :
'y' in k ? Year : 'm' in k ? Month :
'u' in k ? Month : 'U' in k ? Month :
'd' in k ? Day : 'H' in k ? Hour :
'M' in k ? Minute : 'S' in k ? Second : Millisecond
option = 'E' in k ? 2 : 'e' in k ? 1 :
'U' in k ? 2 : 'u' in k ? 1 : 0
push!(slots,slot(i,typ,width,option,locale))
push!(trans,tran)
prefix = ""
params = ()
last_offset = 1

letters = join(keys(SLOT_RULE), "")
for m in eachmatch(Regex("([\\Q$letters\\E])\\1*"), f)
letter = f[m.offset]
typ = SLOT_RULE[letter]

width = length(m.match)
tran = f[last_offset:m.offset-1]

if isempty(params)
prefix = tran
else
slot = tran == "" ? FixedWidthSlot(params...) : DelimitedSlot(params..., tran)
push!(slots,slot)
end

params = (typ,letter,width)
last_offset = m.offset + width
end

tran = last_offset > endof(f) ? r"(?=\s|$)" : f[last_offset:end]
if !isempty(params)
slot = tran == "" ? FixedWidthSlot(params...) : DelimitedSlot(params..., tran)
push!(slots,slot)
end

duplicates(slots) && throw(ArgumentError("Two separate periods of the same type detected"))
return DateFormat(slots,begtran,trans)
return DateFormat(slots,prefix,locale)
end

const SLOTERROR = ArgumentError("Non-digit character encountered")
slotparse(slot,x) = !ismatch(r"[^0-9\s]",x) ? slot.period(x) : throw(SLOTERROR)
function slotparse(slot::Slot{Month},x)
if slot.option == 0
slotparse(slot,x,locale) = !ismatch(r"[^0-9\s]",x) ? slot.parser(x) : throw(SLOTERROR)
function slotparse(slot::Slot{Month},x,locale)
if slot.letter == 'm'
ismatch(r"[^0-9\s]",x) ? throw(SLOTERROR) : return Month(x)
elseif slot.option == 1
return Month(MONTHTOVALUEABBR[slot.locale][lowercase(x)])
elseif slot.letter == 'u'
return Month(MONTHTOVALUEABBR[locale][lowercase(x)])
else
return Month(MONTHTOVALUE[slot.locale][lowercase(x)])
return Month(MONTHTOVALUE[locale][lowercase(x)])
end
end
slotparse(slot::Slot{Millisecond},x) = !ismatch(r"[^0-9\s]",x) ? slot.period(Base.parse(Float64,"."*x)*1000.0) : throw(SLOTERROR)
slotparse(slot::Slot{DayOfWeekSlot},x) = nothing
slotparse(slot::Slot{Millisecond},x,locale) = !ismatch(r"[^0-9\s]",x) ? slot.parser(Base.parse(Float64,"."*x)*1000.0) : throw(SLOTERROR)
slotparse(slot::Slot{DayOfWeekSlot},x,locale) = nothing

function getslot(x,slot::DelimitedSlot,df,cursor)
endind = first(search(x,df.trans[slot.i],nextind(x,cursor)))
function getslot(x,slot::DelimitedSlot,locale,cursor)
endind = first(search(x,slot.transition,nextind(x,cursor)))
if endind == 0 # we didn't find the next delimiter
s = x[cursor:end]
return (endof(x)+1, isdigit(s) ? slotparse(slot,s) : default(slot.period))
index = endof(x)+1
else
s = x[cursor:(endind-1)]
index = nextind(x,endind)
end
return nextind(x,endind), slotparse(slot,x[cursor:(endind-1)])
return index, slotparse(slot,s,locale)
end
getslot(x,slot,df,cursor) = (cursor+slot.width, slotparse(slot,x[cursor:(cursor+slot.width-1)]))
getslot(x,slot,locale,cursor) = (cursor+slot.width, slotparse(slot,x[cursor:(cursor+slot.width-1)], locale))

function parse(x::AbstractString,df::DateFormat)
x = strip(replace(x, r"#.*$", ""))
x = replace(x,df.begtran,"")
x = strip(x)
startswith(x, df.prefix) && (x = replace(x, df.prefix, "", 1))
isempty(x) && throw(ArgumentError("Cannot parse empty format string"))
(typeof(df.slots[1]) <: DelimitedSlot && first(search(x,df.trans[1])) == 0) && throw(ArgumentError("Delimiter mismatch. Couldn't find first delimiter, \"$(df.trans[1])\", in date string"))
if isa(df.slots[1], DelimitedSlot) && first(search(x,df.slots[1].transition)) == 0
throw(ArgumentError("Delimiter mismatch. Couldn't find first delimiter, \"$(df.slots[1].transition)\", in date string"))
end
periods = Period[]
extra = []
cursor = 1
for slot in df.slots
cursor, pe = getslot(x,slot,df,cursor)
pe !== nothing && push!(periods,pe)
cursor, pe = getslot(x,slot,df.locale,cursor)
pe != nothing && (isa(pe,Period) ? push!(periods,pe) : push!(extra,pe))
cursor > endof(x) && break
end
return sort!(periods,rev=true,lt=periodisless)
return vcat(sort!(periods,rev=true,lt=periodisless), extra)
end

slotformat(slot::Slot{Year},dt) = lpad(string(value(slot.period(dt))),slot.width,"0")[(end-slot.width+1):end]
slotformat(slot,dt) = lpad(string(value(slot.period(dt))),slot.width,"0")
function slotformat(slot::Slot{Month},dt)
if slot.option == 0
slotformat(slot::Slot{Year},dt,locale) = lpad(string(value(slot.parser(dt))),slot.width,"0")[(end-slot.width+1):end]
slotformat(slot,dt,locale) = lpad(string(value(slot.parser(dt))),slot.width,"0")
function slotformat(slot::Slot{Month},dt,locale)
if slot.letter == 'm'
return lpad(month(dt),slot.width,"0")
elseif slot.option == 1
return VALUETOMONTHABBR[slot.locale][month(dt)]
elseif slot.letter == 'u'
return VALUETOMONTHABBR[locale][month(dt)]
else
return VALUETOMONTH[slot.locale][month(dt)]
return VALUETOMONTH[locale][month(dt)]
end
end
function slotformat(slot::Slot{DayOfWeekSlot},dt)
if slot.option == 1
return VALUETODAYOFWEEKABBR[slot.locale][dayofweek(dt)]
else # == 2
return VALUETODAYOFWEEK[slot.locale][dayofweek(dt)]
function slotformat(slot::Slot{DayOfWeekSlot},dt,locale)
if slot.letter == 'e'
return VALUETODAYOFWEEKABBR[locale][dayofweek(dt)]
else # == 'E'
return VALUETODAYOFWEEK[locale][dayofweek(dt)]
end
end
slotformat(slot::Slot{Millisecond},dt) = rpad(string(millisecond(dt)/1000.0)[3:end], slot.width, "0")
slotformat(slot::Slot{Millisecond},dt,locale) = rpad(string(millisecond(dt)/1000.0)[3:end], slot.width, "0")

function format(dt::TimeType,df::DateFormat)
f = ""
f = df.prefix
for slot in df.slots
f *= slotformat(slot,dt)
f *= typeof(df.trans[slot.i]) <: Regex ? "" : df.trans[slot.i]
f *= slotformat(slot,dt,df.locale)
if isa(slot, DelimitedSlot)
f *= isa(slot.transition, AbstractString) ? slot.transition : ""
end
end
return f
end
Expand Down
11 changes: 11 additions & 0 deletions test/dates/adjusters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@ end
@test Dates.lastdayofquarter(Dates.DateTime(2014,8,2)) == Dates.DateTime(2014,9,30)
@test Dates.lastdayofquarter(Dates.DateTime(2014,12,2)) == Dates.DateTime(2014,12,31)

first = Dates.Date(2014,1,1)
last = Dates.Date(2014,12,31)
for i = 0:364
dt = first + Dates.Day(i)

@test Dates.firstdayofyear(dt) == first
@test Dates.firstdayofyear(DateTime(dt)) == DateTime(first)

@test Dates.lastdayofyear(dt) == last
@test Dates.lastdayofyear(DateTime(dt)) == DateTime(last)
end

# Adjusters
# Adjuster Constructors
Expand Down
11 changes: 11 additions & 0 deletions test/dates/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,17 @@ gg = "Jan-1996-15"
f = "uuu-yyyy-dd"
@test Dates.DateTime(gg,f) == dt
@test Dates.format(dt,f) == gg
hh = "1996#1#15"
f = "yyyy#m#d"
@test Dates.DateTime(hh,f) == dt
@test Dates.format(dt,f) == hh

# test prefix.
s = "/1996/1/15"
f = "/yyyy/m/d"
@test Dates.DateTime(s,f) == dt
@test Dates.format(dt,f) == s
@test Dates.DateTime("1996/1/15",f) == dt

# from Jiahao
@test Dates.Date("2009年12月01日","yyyy年mm月dd日") == Dates.Date(2009,12,1)
Expand Down

0 comments on commit c321bf8

Please sign in to comment.