Pergi ke kandungan

Modul:Date

Daripada Wikipedia, ensiklopedia bebas.

Pendokumenan untuk modul ini boleh diciptakan diModul:Date/doc

-- Date functions for use by other modules.
-- I18N and time zones are not supported.

localMINUS='−'-- Unicode U+2212 MINUS SIGN
localfloor=math.floor

localDate,DateDiff,diffmt-- forward declarations
localuniq={'unique identifier'}

localfunctionis_date(t)
-- The system used to make a date read-only means there is no unique
-- metatable that is conveniently accessible to check.
returntype(t)=='table'andt._id==uniq
end

localfunctionis_diff(t)
returntype(t)=='table'andgetmetatable(t)==diffmt
end

localfunction_list_join(list,sep)
returntable.concat(list,sep)
end

localfunctioncollection()
-- Return a table to hold items.
return{
n=0,
add=function(self,item)
self.n=self.n+1
self[self.n]=item
end,
join=_list_join,
}
end

localfunctionstrip_to_nil(text)
-- If text is a string, return its trimmed content, or nil if empty.
-- Otherwise return text (convenient when Date fields are provided from
-- another module which may pass a string, a number, or another type).
iftype(text)=='string'then
text=text:match('(%S.-)%s*$')
end
returntext
end

localfunctionis_leap_year(year,calname)
-- Return true if year is a leap year.
ifcalname=='Julian'then
returnyear%4==0
end
return(year%4==0andyear%100~=0)oryear%400==0
end

localfunctiondays_in_month(year,month,calname)
-- Return number of days (1..31) in given month (1..12).
ifmonth==2andis_leap_year(year,calname)then
return29
end
return({31,28,31,30,31,30,31,31,30,31,30,31})[month]
end

localfunctionh_m_s(time)
-- Return hour, minute, second extracted from fraction of a day.
time=floor(time*24*3600+0.5)-- number of seconds
localsecond=time%60
time=floor(time/60)
returnfloor(time/60),time%60,second
end

localfunctionhms(date)
-- Return fraction of a day from date's time, where (0 <= fraction < 1)
-- if the values are valid, but could be anything if outside range.
return(date.hour+(date.minute+date.second/60)/60)/24
end

localfunctionjulian_date(date)
-- Return jd, jdz from a Julian or Gregorian calendar date where
-- jd = Julian date and its fractional part is zero at noon
-- jdz = same, but assume time is 00:00:00 if no time given
-- http:// tondering.dk/claus/cal/julperiod.php#formula
-- Testing shows this works for all dates from year -9999 to 9999!
-- JDN 0 is the 24-hour period starting at noon UTC on Monday
-- 1 January 4713 BC = (-4712, 1, 1) Julian calendar
-- 24 November 4714 BC = (-4713, 11, 24) Gregorian calendar
localoffset
locala=floor((14-date.month)/12)
localy=date.year+4800-a
ifdate.calendar=='Julian'then
offset=floor(y/4)-32083
else
offset=floor(y/4)-floor(y/100)+floor(y/400)-32045
end
localm=date.month+12*a-3
localjd=date.day+floor((153*m+2)/5)+365*y+offset
ifdate.hastimethen
jd=jd+hms(date)-0.5
returnjd,jd
end
returnjd,jd-0.5
end

localfunctionset_date_from_jd(date)
-- Set the fields of table date from its Julian date field.
-- Return true if date is valid.
-- http:// tondering.dk/claus/cal/julperiod.php#formula
-- This handles the proleptic Julian and Gregorian calendars.
-- Negative Julian dates are not defined but they work.
localcalname=date.calendar
locallow,high-- min/max limits for date ranges −9999-01-01 to 9999-12-31
ifcalname=='Gregorian'then
low,high=-1930999.5,5373484.49999
elseifcalname=='Julian'then
low,high=-1931076.5,5373557.49999
else
return
end
localjd=date.jd
ifnot(type(jd)=='number'andlow<=jdandjd<=high)then
return
end
localjdn=floor(jd)
ifdate.hastimethen
localtime=jd-jdn-- 0 <= time < 1
iftime>=0.5then-- if at or after midnight of next day
jdn=jdn+1
time=time-0.5
else
time=time+0.5
end
date.hour,date.minute,date.second=h_m_s(time)
else
date.second=0
date.minute=0
date.hour=0
end
localb,c
ifcalname=='Julian'then
b=0
c=jdn+32082
else-- Gregorian
locala=jdn+32044
b=floor((4*a+3)/146097)
c=a-floor(146097*b/4)
end
locald=floor((4*c+3)/1461)
locale=c-floor(1461*d/4)
localm=floor((5*e+2)/153)
date.day=e-floor((153*m+2)/5)+1
date.month=m+3-12*floor(m/10)
date.year=100*b+d-4800+floor(m/10)
returntrue
end

localfunctionfix_numbers(numbers,y,m,d,H,M,S,partial,hastime,calendar)
-- Put the result of normalizing the given values in table numbers.
-- The result will have valid m, d values if y is valid; caller checks y.
-- The logic of PHP mktime is followed where m or d can be zero to mean
-- the previous unit, and -1 is the one before that, etc.
-- Positive values carry forward.
localdate
ifnot(1<=mandm<=12)then
date=Date(y,1,1)
ifnotdatethenreturnend
date=date+((m-1)..'m')
y,m=date.year,date.month
end
localdays_hms
ifnotpartialthen
ifhastimeandHandMandSthen
ifnot(0<=HandH<=23and
0<=MandM<=59and
0<=SandS<=59)then
days_hms=hms({hour=H,minute=M,second=S})
end
end
ifdays_hmsornot(1<=dandd<=days_in_month(y,m,calendar))then
date=dateorDate(y,m,1)
ifnotdatethenreturnend
date=date+(d-1+(days_hmsor0))
y,m,d=date.year,date.month,date.day
ifdays_hmsthen
H,M,S=date.hour,date.minute,date.second
end
end
end
numbers.year=y
numbers.month=m
numbers.day=d
ifdays_hmsthen
-- Don't set H unless it was valid because a valid H will set hastime.
numbers.hour=H
numbers.minute=M
numbers.second=S
end
end

localfunctionset_date_from_numbers(date,numbers,options)
-- Set the fields of table date from numeric values.
-- Return true if date is valid.
iftype(numbers)~='table'then
return
end
localy=numbers.yearordate.year
localm=numbers.monthordate.month
locald=numbers.dayordate.day
localH=numbers.hour
localM=numbers.minuteordate.minuteor0
localS=numbers.secondordate.secondor0
localneed_fix
ifyandmanddthen
date.partial=nil
ifnot(-9999<=yandy<=9999and
1<=mandm<=12and
1<=dandd<=days_in_month(y,m,date.calendar))then
ifnotdate.want_fixthen
return
end
need_fix=true
end
elseifyanddate.partialthen
ifdornot(-9999<=yandy<=9999)then
return
end
ifmandnot(1<=mandm<=12)then
ifnotdate.want_fixthen
return
end
need_fix=true
end
else
return
end
ifdate.partialthen
H=nil-- ignore any time
M=nil
S=nil
else
ifHthen
-- It is not possible to set M or S without also setting H.
date.hastime=true
else
H=0
end
ifnot(0<=HandH<=23and
0<=MandM<=59and
0<=SandS<=59)then
ifdate.want_fixthen
need_fix=true
else
return
end
end
end
date.want_fix=nil
ifneed_fixthen
fix_numbers(numbers,y,m,d,H,M,S,date.partial,date.hastime,date.calendar)
returnset_date_from_numbers(date,numbers,options)
end
date.year=y-- -9999 to 9999 ('n BC' → year = 1 - n)
date.month=m-- 1 to 12 (may be nil if partial)
date.day=d-- 1 to 31 (* = nil if partial)
date.hour=H-- 0 to 59 (*)
date.minute=M-- 0 to 59 (*)
date.second=S-- 0 to 59 (*)
iftype(options)=='table'then
for_,kinipairs({'am','era','format'})do
ifoptions[k]then
date.options[k]=options[k]
end
end
end
returntrue
end

localfunctionmake_option_table(options1,options2)
-- If options1 is a string, return a table with its settings, or
-- if it is a table, use its settings.
-- Missing options are set from table options2 or defaults.
-- If a default is used, a flag is set so caller knows the value was not intentionally set.
-- Valid option settings are:
-- am: 'am', 'a.m.', 'AM', 'A.M.'
-- 'pm', 'p.m.', 'PM', 'P.M.' (each has same meaning as corresponding item above)
-- era: 'BCMINUS', 'BCNEGATIVE', 'BC', 'B.C.', 'SM', 'B.C.E.', 'AD', 'A.D.', 'CE', 'C.E.'
-- Option am = 'am' does not mean the hour is AM; it means 'am' or 'pm' is used, depending on the hour,
-- and am = 'pm' has the same meaning.
-- Similarly, era = 'BC' means 'BC' is used if year <= 0.
-- BCMINUS displays a MINUS if year < 0 and the display format does not include %{era}.
-- BCNEGATIVE is similar but displays a hyphen.
localresult={bydefault={}}
iftype(options1)=='table'then
result.am=options1.am
result.era=options1.era
elseiftype(options1)=='string'then
-- Example: 'am:AM era:BC' or 'am=AM era=BC'.
foriteminoptions1:gmatch('%S+')do
locallhs,rhs=item:match('^(%w+)[:=](.+)$')
iflhsthen
result[lhs]=rhs
end
end
end
options2=type(options2)=='table'andoptions2or{}
localdefaults={am='am',era='BC'}
fork,vinpairs(defaults)do
ifnotresult[k]then
ifoptions2[k]then
result[k]=options2[k]
else
result[k]=v
result.bydefault[k]=true
end
end
end
returnresult
end

localampm_options={
-- lhs = input text accepted as an am/pm option
-- rhs = code used internally
['am']='am',
['AM']='AM',
['a.m.']='a.m.',
['A.M.']='A.M.',
['pm']='am',-- same as am
['PM']='AM',
['p.m.']='a.m.',
['P.M.']='A.M.',
}

localera_text={
-- Text for displaying an era with a positive year (after adjusting
-- by replacing year with 1 - year if date.year <= 0).
-- options.era = { year<=0, year>0 }
['BCMINUS']={'SM','',isbc=true,sign=MINUS},
['BCNEGATIVE']={'SM','',isbc=true,sign='-'},
['BC']={'SM','',isbc=true},
['B.C.']={'S.M.','',isbc=true},
['BCE']={'SM','',isbc=true},
['B.C.E.']={'S.M.','',isbc=true},
['AD']={'SM','M'},
['A.D.']={'S.M.','M.'},
['CE']={'SM','M'},
['C.E.']={'S.M.','M.'},
}

localfunctionget_era_for_year(era,year)
return(era_text[era]orera_text['BC'])[year>0and2or1]or''
end

localfunctionstrftime(date,format,options)
-- Return date formatted as a string using codes similar to those
-- in the C strftime library function.
localsformat=string.format
localshortcuts={
['%c']='%-I:%M %p %-d %B %-Y %{era}',-- date and time: 2:30 pm 1 April 2016
['%x']='%-d %B %-Y %{era}',-- date: 1 April 2016
['%X']='%-I:%M %p',-- time: 2:30 pm
}
ifshortcuts[format]then
format=shortcuts[format]
end
localcodes={
a={field='dayabbr'},
A={field='dayname'},
b={field='monthabbr'},
B={field='monthname'},
u={fmt='%d',field='dowiso'},
w={fmt='%d',field='dow'},
d={fmt='%02d',fmt2='%d',field='day'},
m={fmt='%02d',fmt2='%d',field='month'},
Y={fmt='%04d',fmt2='%d',field='year'},
H={fmt='%02d',fmt2='%d',field='hour'},
M={fmt='%02d',fmt2='%d',field='minute'},
S={fmt='%02d',fmt2='%d',field='second'},
j={fmt='%03d',fmt2='%d',field='dayofyear'},
I={fmt='%02d',fmt2='%d',field='hour',special='hour12'},
p={field='hour',special='am'},
}
options=make_option_table(options,date.options)
localamopt=options.am
localeraopt=options.era
localfunctionreplace_code(spaces,modifier,id)
localcode=codes[id]
ifcodethen
localfmt=code.fmt
ifmodifier=='-'andcode.fmt2then
fmt=code.fmt2
end
localvalue=date[code.field]
ifnotvaluethen
returnnil-- an undefined field in a partial date
end
localspecial=code.special
ifspecialthen
ifspecial=='hour12'then
value=value%12
value=value==0and12orvalue
elseifspecial=='am'then
localap=({
['a.m.']={'a.m.','p.m.'},
['AM']={'AM','PM'},
['A.M.']={'A.M.','P.M.'},
})[ampm_options[amopt]]or{'am','pm'}
return(spaces==''and''or' ')..(value<12andap[1]orap[2])
end
end
ifcode.field=='year'then
localsign=(era_text[eraopt]or{}).sign
ifnotsignorformat:find('%{era}',1,true)then
sign=''
ifvalue<=0then
value=1-value
end
else
ifvalue>=0then
sign=''
else
value=-value
end
end
returnspaces..sign..sformat(fmt,value)
end
returnspaces..(fmtandsformat(fmt,value)orvalue)
end
end
localfunctionreplace_property(spaces,id)
ifid=='era'then
-- Special case so can use local era option.
localresult=get_era_for_year(eraopt,date.year)
ifresult==''then
return''
end
return(spaces==''and''or' ')..result
end
localresult=date[id]
iftype(result)=='string'then
returnspaces..result
end
iftype(result)=='number'then
returnspaces..tostring(result)
end
iftype(result)=='boolean'then
returnspaces..(resultand'1'or'0')
end
-- This occurs if id is an undefined field in a partial date, or is the name of a function.
returnnil
end
localPERCENT='\127PERCENT\127'
return(format
:gsub('%%%%',PERCENT)
:gsub('(%s*)%%{(%w+)}',replace_property)
:gsub('(%s*)%%(%-?)(%a)',replace_code)
:gsub(PERCENT,'%%')
)
end

localfunction_date_text(date,fmt,options)
-- Return a formatted string representing the given date.
ifnotis_date(date)then
error('date:text: need a date (use "date:text()" with a colon)',2)
end
iftype(fmt)=='string'andfmt:match('%S')then
iffmt:find('%',1,true)then
returnstrftime(date,fmt,options)
end
elseifdate.partialthen
fmt=date.monthand'my'or'y'
else
fmt='dmy'
ifdate.hastimethen
fmt=(date.second>0and'hms 'or'hm ')..fmt
end
end
localfunctionbad_format()
-- For consistency with other format processing, return given format
-- (or cleaned format if original was not a string) if invalid.
returnmw.text.nowiki(fmt)
end
ifdate.partialthen
-- Ignore days in standard formats like 'ymd'.
iffmt=='ym'orfmt=='ymd'then
fmt=date.monthand'%Y-%m %{era}'or'%Y %{era}'
elseiffmt=='my'orfmt=='dmy'orfmt=='mdy'then
fmt=date.monthand'%B %-Y %{era}'or'%-Y %{era}'
elseiffmt=='y'then
fmt=date.monthand'%-Y %{era}'or'%-Y %{era}'
else
returnbad_format()
end
returnstrftime(date,fmt,options)
end
localfunctionhm_fmt()
localplain=make_option_table(options,date.options).bydefault.am
returnplainand'%H:%M'or'%-I:%M %p'
end
localneed_time=date.hastime
localt=collection()
foriteminfmt:gmatch('%S+')do
localf
ifitem=='hm'then
f=hm_fmt()
need_time=false
elseifitem=='hms'then
f='%H:%M:%S'
need_time=false
elseifitem=='ymd'then
f='%Y-%m-%d %{era}'
elseifitem=='mdy'then
f='%B %-d, %-Y %{era}'
elseifitem=='dmy'then
f='%-d %B %-Y %{era}'
else
returnbad_format()
end
t:add(f)
end
fmt=t:join(' ')
ifneed_timethen
fmt=hm_fmt()..' '..fmt
end
returnstrftime(date,fmt,options)
end

localday_info={
-- 0=Aha to 6=Sab
[0]={'Aha','Ahad'},
{'Isn','Isnin'},
{'Sel','Selasa'},
{'Rab','Rabu'},
{'Kha','Khamis'},
{'Jum','Jumaat'},
{'Sab','Sabtu'},
}

localmonth_info={
-- 1=Jan to 12=Dec
{'Jan','Januari'},
{'Feb','Februari'},
{'Mac','Mac'},
{'Apr','April'},
{'Mei','Mei'},
{'Jun','Jun'},
{'Jul','Julai'},
{'Ogo','Ogos'},
{'Sep','September'},
{'Okt','Oktober'},
{'Nov','November'},
{'Dis','Disember'},
}

localfunctionname_to_number(text,translate)
iftype(text)=='string'then
returntranslate[text:lower()]
end
end

localfunctionday_number(text)
returnname_to_number(text,{
ahad=0,ahd=0,minggu=0,sun=0,sunday=0,
isnin=1,isn=1,senin=1,mon=1,monday=1,
selasa=2,sel=2,tue=2,tuesday=2,
rabu=3,rab=3,wed=3,wednesday=3,
khamis=4,kha=4,kamis=4,thu=4,thursday=4,
jumaat=5,jum=5,jumat=5,fri=5,friday=5,
sabtu=6,sab=6,sat=6,saturday=6,
})
end

localfunctionmonth_number(text)
returnname_to_number(text,{
jan=1,januari=1,january=1,
feb=2,februari=2,february=2,
mac=3,maret=3,mar=3,march=3,
apr=4,april=4,
mei=5,may=5,
jun=6,juni=6,june=6,
jul=7,julai=7,juli=7,july=7,
ogo=8,ogos=8,agustus=8,agu=8,ags=8,aug=8,august=8,
sep=9,september=9,sept=9,
okt=10,oktober=10,oct=10,october=10,
nov=11,november=11,
dis=12,disember=12,desember=12,des=12,dec=12,december=12,
})
end

localfunction_list_text(list,fmt)
-- Return a list of formatted strings from a list of dates.
ifnottype(list)=='table'then
error('date:list:text: need "list:text()" with a colon',2)
end
localresult={join=_list_join}
fori,dateinipairs(list)do
result[i]=date:text(fmt)
end
returnresult
end

localfunction_date_list(date,spec)
-- Return a possibly empty numbered table of dates meeting the specification.
-- Dates in the list are in ascending order (oldest date first).
-- The spec should be a string of form "<count> <day> <op>"
-- where each item is optional and
-- count = number of items wanted in list
-- day = abbreviation or name such as Mon or Monday
-- op = >, >=, <, <= (default is > meaning after date)
-- If no count is given, the list is for the specified days in date's month.
-- The default day is date's day.
-- The spec can also be a positive or negative number:
-- -5 is equivalent to '5 <'
-- 5 is equivalent to '5' which is '5 >'
ifnotis_date(date)then
error('date:list: need a date (use "date:list()" with a colon)',2)
end
locallist={text=_list_text}
ifdate.partialthen
returnlist
end
localcount,offset,operation
localops={
['>=']={before=false,include=true},
['>']={before=false,include=false},
['<=']={before=true,include=true},
['<']={before=true,include=false},
}
ifspecthen
iftype(spec)=='number'then
count=floor(spec+0.5)
ifcount<0then
count=-count
operation=ops['<']
end
elseiftype(spec)=='string'then
localnum,day,op=spec:match('^%s*(%d*)%s*(%a*)%s*([<>=]*)%s*$')
ifnotnumthen
returnlist
end
ifnum~=''then
count=tonumber(num)
end
ifday~=''then
localdow=day_number(day:gsub('[sS]$',''))-- accept plural days
ifnotdowthen
returnlist
end
offset=dow-date.dow
end
operation=ops[op]
else
returnlist
end
end
offset=offsetor0
operation=operationorops['>']
localdatefrom,dayfirst,daylast
ifoperation.beforethen
ifoffset>0or(offset==0andnotoperation.include)then
offset=offset-7
end
ifcountthen
ifcount>1then
offset=offset-7*(count-1)
end
datefrom=date+offset
else
daylast=date.day+offset
dayfirst=daylast%7
ifdayfirst==0then
dayfirst=7
end
end
else
ifoffset<0or(offset==0andnotoperation.include)then
offset=offset+7
end
ifcountthen
datefrom=date+offset
else
dayfirst=date.day+offset
daylast=date.monthdays
end
end
ifnotcountthen
ifdaylast<dayfirstthen
returnlist
end
count=floor((daylast-dayfirst)/7)+1
datefrom=Date(date,{day=dayfirst})
end
fori=1,countdo
ifnotdatefromthenbreakend-- exceeds date limits
list[i]=datefrom
datefrom=datefrom+7
end
returnlist
end

-- A table to get the current date/time (UTC), but only if needed.
localcurrent=setmetatable({},{
__index=function(self,key)
locald=os.date('!*t')
self.year=d.year
self.month=d.month
self.day=d.day
self.hour=d.hour
self.minute=d.min
self.second=d.sec
returnrawget(self,key)
end})

localfunctionextract_date(newdate,text)
-- Parse the date/time in text and return n, o where
-- n = table of numbers with date/time fields
-- o = table of options for AM/PM or AD/BC or format, if any
-- or return nothing if date is known to be invalid.
-- Caller determines if the values in n are valid.
-- A year must be positive ('1' to '9999'); use 'BC' for BC.
-- In a y-m-d string, the year must be four digits to avoid ambiguity
-- ('0001' to '9999'). The only way to enter year <= 0 is by specifying
-- the date as three numeric parameters like ymd Date(-1, 1, 1).
-- Dates of form d/m/y, m/d/y, y/m/d are rejected as potentially ambiguous.
localdate,options={},{}
iftext:sub(-1)=='Z'then
-- Extract date/time from a Wikidata timestamp.
-- The year can be 1 to 16 digits but this module handles 1 to 4 digits only.
-- Examples: '+2016-06-21T14:30:00Z', '-0000000180-00-00T00:00:00Z'.
localsign,y,m,d,H,M,S=text:match('^([+%-])(%d+)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)Z$')
ifsignthen
y=tonumber(y)
ifsign=='-'andy>0then
y=-y
end
ify<=0then
options.era='SM'
end
date.year=y
m=tonumber(m)
d=tonumber(d)
H=tonumber(H)
M=tonumber(M)
S=tonumber(S)
ifm==0then
newdate.partial=true
returndate,options
end
date.month=m
ifd==0then
newdate.partial=true
returndate,options
end
date.day=d
ifH>0orM>0orS>0then
date.hour=H
date.minute=M
date.second=S
end
returndate,options
end
return
end
localfunctionextract_ymd(item)
-- Called when no day or month has been set.
localy,m,d=item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')
ifythen
ifdate.yearthen
return
end
ifm:match('^%d%d?$')then
m=tonumber(m)
else
m=month_number(m)
end
ifmthen
date.year=tonumber(y)
date.month=m
date.day=tonumber(d)
returntrue
end
end
end
localfunctionextract_day_or_year(item)
-- Called when a day would be valid, or
-- when a year would be valid if no year has been set and partial is set.
localnumber,suffix=item:match('^(%d%d?%d?%d?)(.*)$')
ifnumberthen
localn=tonumber(number)
if#number<=2andn<=31then
suffix=suffix:lower()
ifsuffix==''orsuffix=='st'orsuffix=='nd'orsuffix=='rd'orsuffix=='th'then
date.day=n
returntrue
end
elseifsuffix==''andnewdate.partialandnotdate.yearthen
date.year=n
returntrue
end
end
end
localfunctionextract_month(item)
-- A month must be given as a name or abbreviation; a number could be ambiguous.
localm=month_number(item)
ifmthen
date.month=m
returntrue
end
end
localfunctionextract_time(item)
localh,m,s=item:match('^(%d%d?):(%d%d)(:?%d*)$')
ifdate.hourornoththen
return
end
ifs~=''then
s=s:match('^:(%d%d)$')
ifnotsthen
return
end
end
date.hour=tonumber(h)
date.minute=tonumber(m)
date.second=tonumber(s)-- nil if empty string
returntrue
end
localitem_count=0
localindex_time
localfunctionset_ampm(item)
localH=date.hour
ifHandnotoptions.amandindex_time+1==item_countthen
options.am=ampm_options[item]-- caller checked this is not nil
ifitem:match('^[Aa]')then
ifnot(1<=HandH<=12)then
return
end
ifH==12then
date.hour=0
end
else
ifnot(1<=HandH<=23)then
return
end
ifH<=11then
date.hour=H+12
end
end
returntrue
end
end
foritemintext:gsub(',',' '):gsub(' ',' '):gmatch('%S+')do
item_count=item_count+1
ifera_text[item]then
-- Era is accepted in peculiar places.
ifoptions.erathen
return
end
options.era=item
elseifampm_options[item]then
ifnotset_ampm(item)then
return
end
elseifitem:find(':',1,true)then
ifnotextract_time(item)then
return
end
index_time=item_count
elseifdate.dayanddate.monththen
ifdate.yearthen
return-- should be nothing more so item is invalid
end
ifnotitem:match('^(%d%d?%d?%d?)$')then
return
end
date.year=tonumber(item)
elseifdate.daythen
ifnotextract_month(item)then
return
end
elseifdate.monththen
ifnotextract_day_or_year(item)then
return
end
elseifextract_month(item)then
options.format='mdy'
elseifextract_ymd(item)then
options.format='ymd'
elseifextract_day_or_year(item)then
ifdate.daythen
options.format='dmy'
end
else
return
end
end
ifnotdate.yearordate.year==0then
return
end
localera=era_text[options.era]
iferaandera.isbcthen
date.year=1-date.year
end
returndate,options
end

localfunctionautofill(date1,date2)
-- Fill any missing month or day in each date using the
-- corresponding component from the other date, if present,
-- or with 1 if both dates are missing the month or day.
-- This gives a good result for calculating the difference
-- between two partial dates when no range is wanted.
-- Return filled date1, date2 (two full dates).
localfunctionfilled(a,b)
localfillmonth,fillday
ifnota.monththen
fillmonth=b.monthor1
end
ifnota.daythen
fillday=b.dayor1
end
iffillmonthorfilldaythen-- need to create a new date
if(fillmonthora.month)==2and(filldayora.day)==29then
-- Avoid invalid date, for example with {{age|2013|29 Feb 2016}} or {{age|Feb 2013|29 Jan 2015}}.
ifnotis_leap_year(a.year,a.calendar)then
fillday=28
end
end
a=Date(a,{month=fillmonth,day=fillday})
end
returna
end
returnfilled(date1,date2),filled(date2,date1)
end

localfunctiondate_add_sub(lhs,rhs,is_sub)
-- Return a new date from calculating (lhs + rhs) or (lhs - rhs),
-- or return nothing if invalid.
-- The result is nil if the calculated date exceeds allowable limits.
-- Caller ensures that lhs is a date; its properties are copied for the new date.
iflhs.partialthen
-- Adding to a partial is not supported.
-- Can subtract a date or partial from a partial, but this is not called for that.
return
end
localfunctionis_prefix(text,word,minlen)
localn=#text
return(minlenor1)<=nandn<=#wordandtext==word:sub(1,n)
end
localfunctiondo_days(n)
localforcetime,jd
iffloor(n)==nthen
jd=lhs.jd
else
forcetime=notlhs.hastime
jd=lhs.jdz
end
jd=jd+(is_suband-norn)
ifforcetimethen
jd=tostring(jd)
ifnotjd:find('.',1,true)then
jd=jd..'.0'
end
end
returnDate(lhs,'juliandate',jd)
end
iftype(rhs)=='number'then
-- Add/subtract days, including fractional days.
returndo_days(rhs)
end
iftype(rhs)=='string'then
-- rhs is a single component like '26m' or '26 months' (with optional sign).
-- Fractions like '3.25d' are accepted for the units which are handled as days.
localsign,numstr,id=rhs:match('^%s*([+-]?)([%d%.]+)%s*(%a+)$')
ifsignthen
ifsign=='-'then
is_sub=not(is_subandtrueorfalse)
end
localy,m,days
localnum=tonumber(numstr)
ifnotnumthen
return
end
id=id:lower()
ifis_prefix(id,'years')then
y=num
m=0
elseifis_prefix(id,'months')then
y=floor(num/12)
m=num%12
elseifis_prefix(id,'weeks')then
days=num*7
elseifis_prefix(id,'days')then
days=num
elseifis_prefix(id,'hours')then
days=num/24
elseifis_prefix(id,'minutes',3)then
days=num/(24*60)
elseifis_prefix(id,'seconds')then
days=num/(24*3600)
else
return
end
ifdaysthen
returndo_days(days)
end
ifnumstr:find('.',1,true)then
return
end
ifis_subthen
y=-y
m=-m
end
assert(-11<=mandm<=11)
y=lhs.year+y
m=lhs.month+m
ifm>12then
y=y+1
m=m-12
elseifm<1then
y=y-1
m=m+12
end
locald=math.min(lhs.day,days_in_month(y,m,lhs.calendar))
returnDate(lhs,y,m,d)
end
end
ifis_diff(rhs)then
localdays=rhs.age_days
if(is_suborfalse)~=(rhs.isnegativeorfalse)then
days=-days
end
returnlhs+days
end
end

localfull_date_only={
dayabbr=true,
dayname=true,
dow=true,
dayofweek=true,
dowiso=true,
dayofweekiso=true,
dayofyear=true,
gsd=true,
juliandate=true,
jd=true,
jdz=true,
jdnoon=true,
}

-- Metatable for a date's calculated fields.
localdatemt={
__index=function(self,key)
ifrawget(self,'partial')then
iffull_date_only[key]thenreturnend
ifkey=='monthabbr'orkey=='monthdays'orkey=='monthname'then
ifnotself.monththenreturnend
end
end
localvalue
ifkey=='dayabbr'then
value=day_info[self.dow][1]
elseifkey=='dayname'then
value=day_info[self.dow][2]
elseifkey=='dow'then
value=(self.jdnoon+1)%7-- day-of-week 0=Sun to 6=Sat
elseifkey=='dayofweek'then
value=self.dow
elseifkey=='dowiso'then
value=(self.jdnoon%7)+1-- ISO day-of-week 1=Mon to 7=Sun
elseifkey=='dayofweekiso'then
value=self.dowiso
elseifkey=='dayofyear'then
localfirst=Date(self.year,1,1,self.calendar).jdnoon
value=self.jdnoon-first+1-- day-of-year 1 to 366
elseifkey=='era'then
-- Era text (never a negative sign) from year and options.
value=get_era_for_year(self.options.era,self.year)
elseifkey=='format'then
value=self.options.formator'dmy'
elseifkey=='gsd'then
-- GSD = 1 from 00:00:00 to 23:59:59 on 1 January 1 AD Gregorian calendar,
-- which is from jd 1721425.5 to 1721426.49999.
value=floor(self.jd-1721424.5)
elseifkey=='juliandate'orkey=='jd'orkey=='jdz'then
localjd,jdz=julian_date(self)
rawset(self,'juliandate',jd)
rawset(self,'jd',jd)
rawset(self,'jdz',jdz)
returnkey=='jdz'andjdzorjd
elseifkey=='jdnoon'then
-- Julian date at noon (an integer) on the calendar day when jd occurs.
value=floor(self.jd+0.5)
elseifkey=='isleapyear'then
value=is_leap_year(self.year,self.calendar)
elseifkey=='monthabbr'then
value=month_info[self.month][1]
elseifkey=='monthdays'then
value=days_in_month(self.year,self.month,self.calendar)
elseifkey=='monthname'then
value=month_info[self.month][2]
end
ifvalue~=nilthen
rawset(self,key,value)
returnvalue
end
end,
}

-- Date operators.
localfunctionmt_date_add(lhs,rhs)
ifnotis_date(lhs)then
lhs,rhs=rhs,lhs-- put date on left (it must be a date for this to have been called)
end
returndate_add_sub(lhs,rhs)
end

localfunctionmt_date_sub(lhs,rhs)
ifis_date(lhs)then
ifis_date(rhs)then
returnDateDiff(lhs,rhs)
end
returndate_add_sub(lhs,rhs,true)
end
end

localfunctionmt_date_concat(lhs,rhs)
returntostring(lhs)..tostring(rhs)
end

localfunctionmt_date_tostring(self)
returnself:text()
end

localfunctionmt_date_eq(lhs,rhs)
-- Return true if dates identify same date/time where, for example,
-- Date(-4712, 1, 1, 'Julian') == Date(-4713, 11, 24, 'Gregorian') is true.
-- This is called only if lhs and rhs have the same type and the same metamethod.
iflhs.partialorrhs.partialthen
-- One date is partial; the other is a partial or a full date.
-- The months may both be nil, but must be the same.
returnlhs.year==rhs.yearandlhs.month==rhs.monthandlhs.calendar==rhs.calendar
end
returnlhs.jdz==rhs.jdz
end

localfunctionmt_date_lt(lhs,rhs)
-- Return true if lhs < rhs, for example,
-- Date('1 Jan 2016') < Date('06:00 1 Jan 2016') is true.
-- This is called only if lhs and rhs have the same type and the same metamethod.
iflhs.partialorrhs.partialthen
-- One date is partial; the other is a partial or a full date.
iflhs.calendar~=rhs.calendarthen
returnlhs.calendar=='Julian'
end
iflhs.partialthen
lhs=lhs.partial.first
end
ifrhs.partialthen
rhs=rhs.partial.first
end
end
returnlhs.jdz<rhs.jdz
end

--[[ Examples of syntax to construct a date:
Date(y, m, d, 'julian') default calendar is 'gregorian'
Date(y, m, d, H, M, S, 'julian')
Date('juliandate', jd, 'julian') if jd contains "." text output includes H:M:S
Date('currentdate')
Date('currentdatetime')
Date('1 April 1995', 'julian') parse date from text
Date('1 April 1995 AD', 'julian') using an era sets a flag to do the same for output
Date('04:30:59 1 April 1995', 'julian')
Date(date) copy of an existing date
Date(date, t) same, updated with y,m,d,H,M,S fields from table t
Date(t) date with y,m,d,H,M,S fields from table t
]]
functionDate(...)-- for forward declaration above
-- Return a table holding a date assuming a uniform calendar always applies
-- (proleptic Gregorian calendar or proleptic Julian calendar), or
-- return nothing if date is invalid.
-- A partial date has a valid year, however its month may be nil, and
-- its day and time fields are nil.
-- Field partial is set to false (if a full date) or a table (if a partial date).
localcalendars={julian='Julian',gregorian='Gregorian'}
localnewdate={
_id=uniq,
calendar='Gregorian',-- default is Gregorian calendar
hastime=false,-- true if input sets a time
hour=0,-- always set hour/minute/second so don't have to handle nil
minute=0,
second=0,
options={},
list=_date_list,
subtract=function(self,rhs,options)
returnDateDiff(self,rhs,options)
end,
text=_date_text,
}
localargtype,datetext,is_copy,jd_number,tnums
localnumindex=0
localnumfields={'year','month','day','hour','minute','second'}
localnumbers={}
for_,vinipairs({...})do
v=strip_to_nil(v)
localvlower=type(v)=='string'andv:lower()ornil
ifv==nilthen
-- Ignore empty arguments after stripping so modules can directly pass template parameters.
elseifcalendars[vlower]then
newdate.calendar=calendars[vlower]
elseifvlower=='partial'then
newdate.partial=true
elseifvlower=='fix'then
newdate.want_fix=true
elseifis_date(v)then
-- Copy existing date (items can be overridden by other arguments).
ifis_copyortnumsthen
return
end
is_copy=true
newdate.calendar=v.calendar
newdate.partial=v.partial
newdate.hastime=v.hastime
newdate.options=v.options
newdate.year=v.year
newdate.month=v.month
newdate.day=v.day
newdate.hour=v.hour
newdate.minute=v.minute
newdate.second=v.second
elseiftype(v)=='table'then
iftnumsthen
return
end
tnums={}
localtfields={year=1,month=1,day=1,hour=2,minute=2,second=2}
fortk,tvinpairs(v)do
iftfields[tk]then
tnums[tk]=tonumber(tv)
end
iftfields[tk]==2then
newdate.hastime=true
end
end
else
localnum=tonumber(v)
ifnotnumandargtype=='setdate'andnumindex==1then
num=month_number(v)
end
ifnumthen
ifnotargtypethen
argtype='setdate'
end
ifargtype=='setdate'andnumindex<6then
numindex=numindex+1
numbers[numfields[numindex]]=num
elseifargtype=='juliandate'andnotjd_numberthen
jd_number=num
iftype(v)=='string'then
ifv:find('.',1,true)then
newdate.hastime=true
end
elseifnum~=floor(num)then
-- The given value was a number. The time will be used
-- if the fractional part is nonzero.
newdate.hastime=true
end
else
return
end
elseifargtypethen
return
elseiftype(v)=='string'then
ifv=='currentdate'orv=='currentdatetime'orv=='juliandate'then
argtype=v
else
argtype='datetext'
datetext=v
end
else
return
end
end
end
ifargtype=='datetext'then
iftnumsornotset_date_from_numbers(newdate,extract_date(newdate,datetext))then
return
end
elseifargtype=='juliandate'then
newdate.partial=nil
newdate.jd=jd_number
ifnotset_date_from_jd(newdate)then
return
end
elseifargtype=='currentdate'orargtype=='currentdatetime'then
newdate.partial=nil
newdate.year=current.year
newdate.month=current.month
newdate.day=current.day
ifargtype=='currentdatetime'then
newdate.hour=current.hour
newdate.minute=current.minute
newdate.second=current.second
newdate.hastime=true
end
newdate.calendar='Gregorian'-- ignore any given calendar name
elseifargtype=='setdate'then
iftnumsornotset_date_from_numbers(newdate,numbers)then
return
end
elseifnot(is_copyortnums)then
return
end
iftnumsthen
newdate.jd=nil-- force recalculation in case jd was set before changes from tnums
ifnotset_date_from_numbers(newdate,tnums)then
return
end
end
ifnewdate.partialthen
localyear=newdate.year
localmonth=newdate.month
localfirst=Date(year,monthor1,1,newdate.calendar)
month=monthor12
locallast=Date(year,month,days_in_month(year,month),newdate.calendar)
newdate.partial={first=first,last=last}
else
newdate.partial=false-- avoid index lookup
end
setmetatable(newdate,datemt)
localreadonly={}
localmt={
__index=newdate,
__newindex=function(t,k,v)error('date.'..tostring(k)..' is read-only',2)end,
__add=mt_date_add,
__sub=mt_date_sub,
__concat=mt_date_concat,
__tostring=mt_date_tostring,
__eq=mt_date_eq,
__lt=mt_date_lt,
}
returnsetmetatable(readonly,mt)
end

localfunction_diff_age(diff,code,options)
-- Return a tuple of integer values from diff as specified by code, except that
-- each integer may be a list of two integers for a diff with a partial date, or
-- return nil if the code is not supported.
-- If want round, the least significant unit is rounded to nearest whole unit.
-- For a duration, an extra day is added.
localwantround,wantduration,wantrange
iftype(options)=='table'then
wantround=options.round
wantduration=options.duration
wantrange=options.range
else
wantround=options
end
ifnotis_diff(diff)then
localf=wantdurationand'duration'or'age'
error(f..': need a date difference (use "diff:'..f..'() "with a colon)',2)
end
ifdiff.partialthen
-- Ignore wantround, wantduration.
localfunctionchoose(v)
iftype(v)=='table'then
ifnotwantrangeorv[1]==v[2]then
-- Example: Date('partial', 2005) - Date('partial', 2001) gives
-- diff.years = { 3, 4 } to show the range of possible results.
-- If do not want a range, choose the second value as more expected.
returnv[2]
end
end
returnv
end
ifcode=='ym'orcode=='ymd'then
ifnotwantrangeanddiff.iszerothen
-- This avoids an unexpected result such as
-- Date('partial', 2001) - Date('partial', 2001)
-- giving diff = { years = 0, months = { 0, 11 } }
-- which would be reported as 0 years and 11 months.
return0,0
end
returnchoose(diff.partial.years),choose(diff.partial.months)
end
ifcode=='y'then
returnchoose(diff.partial.years)
end
ifcode=='m'orcode=='w'orcode=='d'then
returnchoose({diff.partial.mindiff:age(code),diff.partial.maxdiff:age(code)})
end
returnnil
end
localextra_days=wantdurationand1or0
ifcode=='wd'orcode=='w'orcode=='d'then
localoffset=wantroundand0.5or0
localdays=diff.age_days+extra_days
ifcode=='wd'orcode=='d'then
days=floor(days+offset)
ifcode=='d'then
returndays
end
returnfloor(days/7),days%7
end
returnfloor(days/7+offset)
end
localH,M,S=diff.hours,diff.minutes,diff.seconds
ifcode=='dh'orcode=='dhm'orcode=='dhms'orcode=='h'orcode=='hm'orcode=='hms'then
localdays=floor(diff.age_days+extra_days)
localinc_hour
ifwantroundthen
ifcode=='dh'orcode=='h'then
ifM>=30then
inc_hour=true
end
elseifcode=='dhm'orcode=='hm'then
ifS>=30then
M=M+1
ifM>=60then
M=0
inc_hour=true
end
end
else
-- Nothing needed because S is an integer.
end
ifinc_hourthen
H=H+1
ifH>=24then
H=0
days=days+1
end
end
end
ifcode=='dh'orcode=='dhm'orcode=='dhms'then
ifcode=='dh'then
returndays,H
elseifcode=='dhm'then
returndays,H,M
else
returndays,H,M,S
end
end
localhours=days*24+H
ifcode=='h'then
returnhours
elseifcode=='hm'then
returnhours,M
end
returnhours,M,S
end
ifwantroundthen
localinc_hour
ifcode=='ymdh'orcode=='ymwdh'then
ifM>=30then
inc_hour=true
end
elseifcode=='ymdhm'orcode=='ymwdhm'then
ifS>=30then
M=M+1
ifM>=60then
M=0
inc_hour=true
end
end
elseifcode=='ymd'orcode=='ymwd'orcode=='yd'orcode=='md'then
ifH>=12then
extra_days=extra_days+1
end
end
ifinc_hourthen
H=H+1
ifH>=24then
H=0
extra_days=extra_days+1
end
end
end
localy,m,d=diff.years,diff.months,diff.days
ifextra_days>0then
d=d+extra_days
ifd>28orcode=='yd'then
-- Recalculate in case have passed a month.
diff=diff.date1+extra_days-diff.date2
y,m,d=diff.years,diff.months,diff.days
end
end
ifcode=='ymd'then
returny,m,d
elseifcode=='yd'then
ify>0then
-- It is known that diff.date1 > diff.date2.
diff=diff.date1-(diff.date2+(y..'y'))
end
returny,floor(diff.age_days)
elseifcode=='md'then
returny*12+m,d
elseifcode=='ym'orcode=='m'then
ifwantroundthen
ifd>=16then
m=m+1
ifm>=12then
m=0
y=y+1
end
end
end
ifcode=='ym'then
returny,m
end
returny*12+m
elseifcode=='ymw'then
localweeks=floor(d/7)
ifwantroundthen
localdays=d%7
ifdays>3or(days==3andH>=12)then
weeks=weeks+1
end
end
returny,m,weeks
elseifcode=='ymwd'then
returny,m,floor(d/7),d%7
elseifcode=='ymdh'then
returny,m,d,H
elseifcode=='ymwdh'then
returny,m,floor(d/7),d%7,H
elseifcode=='ymdhm'then
returny,m,d,H,M
elseifcode=='ymwdhm'then
returny,m,floor(d/7),d%7,H,M
end
ifcode=='y'then
ifwantroundandm>=6then
y=y+1
end
returny
end
returnnil
end

localfunction_diff_duration(diff,code,options)
iftype(options)~='table'then
options={round=options}
end
options.duration=true
return_diff_age(diff,code,options)
end

-- Metatable for some operations on date differences.
diffmt={-- for forward declaration above
__concat=function(lhs,rhs)
returntostring(lhs)..tostring(rhs)
end,
__tostring=function(self)
returntostring(self.age_days)
end,
__index=function(self,key)
localvalue
ifkey=='age_days'then
ifrawget(self,'partial')then
localfunctionjdz(date)
return(date.partialanddate.partial.firstordate).jdz
end
value=jdz(self.date1)-jdz(self.date2)
else
value=self.date1.jdz-self.date2.jdz
end
end
ifvalue~=nilthen
rawset(self,key,value)
returnvalue
end
end,
}

functionDateDiff(date1,date2,options)-- for forward declaration above
-- Return a table with the difference between two dates (date1 - date2).
-- The difference is negative if date1 is older than date2.
-- Return nothing if invalid.
-- If d = date1 - date2 then
-- date1 = date2 + d
-- If date1 >= date2 and the dates have no H:M:S time specified then
-- date1 = date2 + (d.years..'y') + (d.months..'m') + d.days
-- where the larger time units are added first.
-- The result of Date(2015,1,x) + '1m' is Date(2015,2,28) for
-- x = 28, 29, 30, 31. That means, for example,
-- d = Date(2015,3,3) - Date(2015,1,31)
-- gives d.years, d.months, d.days = 0, 1, 3 (excluding date1).
ifnot(is_date(date1)andis_date(date2)anddate1.calendar==date2.calendar)then
return
end
localwantfill
iftype(options)=='table'then
wantfill=options.fill
end
localisnegative=false
localiszero=false
ifdate1<date2then
isnegative=true
date1,date2=date2,date1
elseifdate1==date2then
iszero=true
end
-- It is known that date1 >= date2 (period is from date2 to date1).
ifdate1.partialordate2.partialthen
-- Two partial dates might have timelines:
---------------------A=================B--- date1 is from A to B inclusive
--------C=======D-------------------------- date2 is from C to D inclusive
-- date1 > date2 iff A > C (date1.partial.first > date2.partial.first)
-- The periods can overlap ('April 2001' - '2001'):
-------------A===B------------------------- A=2001-04-01 B=2001-04-30
--------C=====================D------------ C=2001-01-01 D=2001-12-31
ifwantfillthen
date1,date2=autofill(date1,date2)
else
localfunctionzdiff(date1,date2)
localdiff=date1-date2
ifdiff.isnegativethen
returndate1-date1-- a valid diff in case we call its methods
end
returndiff
end
localfunctiongetdate(date,which)
returndate.partialanddate.partial[which]ordate
end
localmaxdiff=zdiff(getdate(date1,'last'),getdate(date2,'first'))
localmindiff=zdiff(getdate(date1,'first'),getdate(date2,'last'))
localyears,months
ifmaxdiff.years==mindiff.yearsthen
years=maxdiff.years
ifmaxdiff.months==mindiff.monthsthen
months=maxdiff.months
else
months={mindiff.months,maxdiff.months}
end
else
years={mindiff.years,maxdiff.years}
end
returnsetmetatable({
date1=date1,
date2=date2,
partial={
years=years,
months=months,
maxdiff=maxdiff,
mindiff=mindiff,
},
isnegative=isnegative,
iszero=iszero,
age=_diff_age,
duration=_diff_duration,
},diffmt)
end
end
localy1,m1=date1.year,date1.month
localy2,m2=date2.year,date2.month
localyears=y1-y2
localmonths=m1-m2
locald1=date1.day+hms(date1)
locald2=date2.day+hms(date2)
localdays,time
ifd1>=d2then
days=d1-d2
else
months=months-1
-- Get days in previous month (before the "to" date) given December has 31 days.
localdpm=m1>1anddays_in_month(y1,m1-1,date1.calendar)or31
ifd2>=dpmthen
days=d1-hms(date2)
else
days=dpm-d2+d1
end
end
ifmonths<0then
years=years-1
months=months+12
end
days,time=math.modf(days)
localH,M,S=h_m_s(time)
returnsetmetatable({
date1=date1,
date2=date2,
partial=false,-- avoid index lookup
years=years,
months=months,
days=days,
hours=H,
minutes=M,
seconds=S,
isnegative=isnegative,
iszero=iszero,
age=_diff_age,
duration=_diff_duration,
},diffmt)
end

return{
_current=current,
_Date=Date,
_days_in_month=days_in_month,
}