Jump to content
Wikipedia The Free Encyclopedia

Module:Date

From Wikipedia, the free encyclopedia
Module documentation[view] [edit] [history] [purge]
Warning This Lua module is used on approximately 1,790,000 pages, or roughly 3% of all pages .
To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Consider discussing changes on the talk page before implementing them.

This module provides date functions for use by other modules. Dates in the Gregorian calendar and the Julian calendar are supported, from 9999 BCE to 9999 CE. The calendars are proleptic—they are assumed to apply at all times with no irregularities.

A date, with an optional time, can be specified in a variety of formats, and can be converted for display using a variety of formats, for example, 1 April 2016 or April 1, 2016. The properties of a date include its Julian date and its Gregorian serial date, as well as the day-of-week and day-of-year.

Dates can be compared (for example, date1 <= date2), and can be used with add or subtract (for example, date + '3 months'). The difference between two dates can be determined with date1 - date2. These operations work with both Gregorian and Julian calendar dates, but date1 - date2 is nil if the two dates use different calendars.

The module provides the following items.

Export Description
_current Table with the current year, month, day, hour, minute, second.
_Date Function that returns a table for a specified date.
_days_in_month Function that returns the number of days in a month.

The following has examples of using the module:

Formatted output

A date can be formatted as text.

localDate=require('Module:Date')._Date
localtext=Date(2016,7,1):text()-- result is '1 July 2016'
localtext=Date(2016,7,1):text('%-d %B')-- result is '1 July'
localtext=Date('1 July 2016'):text('mdy')-- result is 'July 1, 2016'

The following simplified formatting codes are available.

Code Result
hm hour:minute, with "am" or "pm" or variant, if specified (14:30 or 2:30 pm or variant)
hms hour:minute:second (14:30:45)
ymd year-month-day (2016年07月01日)
mdy month day, year (July 1, 2016)
dmy day month year (1 July 2016)

The following formatting codes (similar to strftime) are available.

Code Result
%a Day abbreviation: Mon, Tue, ...
%A Day name: Monday, Tuesday, ...
%u Day of week: 1 to 7 (Monday to Sunday)
%w Day of week: 0 to 6 (Sunday to Saturday)
%d Day of month zero-padded: 01 to 31
%b Month abbreviation: Jan to Dec
%B Month name: January to December
%m Month zero-padded: 01 to 12
%Y Year zero-padded: 0012, 0120, 1200
%H Hour 24-hour clock zero-padded: 00 to 23
%I Hour 12-hour clock zero-padded: 01 to 12
%p AM or PM or as in options
%M Minute zero-padded: 00 to 59
%S Second zero-padded: 00 to 59
%j Day of year zero-padded: 001 to 366
%-d Day of month: 1 to 31
%-m Month: 1 to 12
%-Y Year: 12, 120, 1200
%-H Hour: 0 to 23
%-M Minute: 0 to 59
%-S Second: 0 to 59
%-j Day of year: 1 to 366
%-I Hour: 1 to 12
%% %

In addition, %{property} (where property is any property of a date) can be used.

For example, Date('1 Feb 2015 14:30:45 A.D.') has the following properties.

Code Result
%{calendar} Gregorian
%{year} 2015
%{month} 2
%{day} 1
%{hour} 14
%{minute} 30
%{second} 45
%{dayabbr} Sun
%{dayname} Sunday
%{dayofweek} 0
%{dow} 0 (same as dayofweek)
%{dayofweekiso} 7
%{dowiso} 7 (same as dayofweekiso)
%{dayofyear} 32
%{era} A.D.
%{gsd} 735630 (numbers of days from 1 January 1 CE; the first is day 1)
%{juliandate} 2457055.1046875 (Julian day)
%{jd} 2457055.1046875 (same as juliandate)
%{isleapyear} false
%{monthdays} 28
%{monthabbr} Feb
%{monthname} February

Some shortcuts are available. Given date = Date('1 Feb 2015 14:30'), the following results would occur.

Code Description Example result Equivalent format
date:text('%c') date and time 2:30 pm 1 February 2015 %-I:%M %p %-d %B %-Y %{era}
date:text('%x') date 1 February 2015 %-d %B %-Y %{era}
date:text('%X') time 2:30 pm %-I:%M %p

Julian date

The following has an example of converting a Julian date to a date, then obtaining information about the date.

-- Code -- Result
Date=require('Module:Date')._Date
date=Date('juliandate',320)
number=date.gsd-- -1721105
number=date.jd-- 320
text=date.dayname-- Saturday
text=date:text()-- 9 October 4713&nbsp;BC
text=date:text('%Y-%m-%d')-- 4713年10月09日
text=date:text('%{era} %Y-%m-%d')-- BC 4713年10月09日
text=date:text('%Y-%m-%d %{era}')-- 4713年10月09日&nbsp;BC
text=date:text('%Y-%m-%d %{era}','era=B.C.E.')-- 4713年10月09日&nbsp;B.C.E.
text=date:text('%Y-%m-%d','era=BCNEGATIVE')-- -4712年10月09日
text=date:text('%Y-%m-%d','era=BCMINUS')-- −4712年10月09日 (uses Unicode MINUS SIGN U+2212)
text=Date('juliandate',320):text('%{gsd} %{jd}')-- -1721105 320
text=Date('Oct 9, 4713 B.C.E.'):text('%{gsd} %{jd}')-- -1721105 320
text=Date(-4712,10,9):text('%{gsd} %{jd}')-- -1721105 320

Date differences

The difference between two dates can be determined with date1 - date2. The result is valid if both dates use the Gregorian calendar or if both dates use the Julian calendar, otherwise the result is nil. An age and duration can be calculated from a date difference.

For example:

-- Code -- Result
Date=require('Module:Date')._Date
date1=Date('21 Mar 2015')
date2=Date('4 Dec 1999')
diff=date1-date2
d=diff.age_days-- 5586
y,m,d=diff.years,diff.months,diff.days-- 15, 3, 17 (15 years + 3 months + 17 days)
y,m,d=diff:age('ymd')-- 15, 3, 17
y,m,w,d=diff:age('ymwd')-- 15, 3, 2, 3 (15 years + 3 months + 2 weeks + 3 days)
y,m,w,d=diff:duration('ymwd')-- 15, 3, 2, 4
d=diff:duration('d')-- 5587 (a duration includes the final day)

A date difference holds the original dates except they are swapped so diff.date1 >= diff.date2 (diff.date1 is the more recent date). This is shown in the following.

date1=Date('21 Mar 2015')
date2=Date('4 Dec 1999')
diff=date1-date2
neg=diff.isnegative-- false
text=diff.date1:text()-- 21 March 2015
text=diff.date2:text()-- 4 December 1999
diff=date2-date1
neg=diff.isnegative-- true (dates have been swapped)
text=diff.date1:text()-- 21 March 2015
text=diff.date2:text()-- 4 December 1999

A date difference also holds a time difference:

date1=Date('8 Mar 2016 0:30:45')
date2=Date('19 Jan 2014 22:55')
diff=date1-date2
y,m,d=diff.years,diff.months,diff.days-- 2, 1, 17
H,M,S=diff.hours,diff.minutes,diff.seconds-- 1, 35, 45

A date difference can be added to a date, or subtracted from a date.

date1=Date('8 Mar 2016 0:30:45')
date2=Date('19 Jan 2014 22:55')
diff=date1-date2
date3=date2+diff
date4=date1-diff
text=date3:text('ymd hms')-- 2016年03月08日 00:30:45
text=date4:text('ymd hms')-- 2014年01月19日 22:55:00
equal=(date1==date3)-- true
equal=(date2==date4)-- true

The age and duration methods of a date difference accept a code that identifies the components that should be returned. An extra day is included for the duration method because it includes the final day.

Code Returned values
'ymwd' years, months, weeks, days
'ymd' years, months, days
'ym' years, months
'y' years
'm' months
'wd' weeks, days
'w' weeks
'd' days
The above documentation is transcluded from Module:Date/doc. (edit | history)
Editors can experiment in this module's sandbox (edit | diff) and testcases (edit | run) pages.
Subpages of this module.

 -- 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://www.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://www.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.', 'BCE', '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']={'BC','',isbc=true,sign=MINUS},
 ['BCNEGATIVE']={'BC','',isbc=true,sign='-'},
 ['BC']={'BC','',isbc=true},
 ['B.C.']={'B.C.','',isbc=true},
 ['BCE']={'BCE','',isbc=true},
 ['B.C.E.']={'B.C.E.','',isbc=true},
 ['AD']={'BC','AD'},
 ['A.D.']={'B.C.','A.D.'},
 ['CE']={'BCE','CE'},
 ['C.E.']={'B.C.E.','C.E.'},
 }

 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'&nbsp;')..(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'&nbsp;')..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='127円PERCENT127円'
 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=Sun to 6=Sat
 [0]={'Sun','Sunday'},
 {'Mon','Monday'},
 {'Tue','Tuesday'},
 {'Wed','Wednesday'},
 {'Thu','Thursday'},
 {'Fri','Friday'},
 {'Sat','Saturday'},
 }

 localmonth_info={
 -- 1=Jan to 12=Dec
 {'Jan','January'},
 {'Feb','February'},
 {'Mar','March'},
 {'Apr','April'},
 {'May','May'},
 {'Jun','June'},
 {'Jul','July'},
 {'Aug','August'},
 {'Sep','September'},
 {'Oct','October'},
 {'Nov','November'},
 {'Dec','December'},
 }

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

 localfunctionday_number(text)
 returnname_to_number(text,{
 sun=0,sunday=0,
 mon=1,monday=1,
 tue=2,tuesday=2,
 wed=3,wednesday=3,
 thu=4,thursday=4,
 fri=5,friday=5,
 sat=6,saturday=6,
 })
 end

 localfunctionmonth_number(text)
 returnname_to_number(text:gsub('%.',''),{
 jan=1,january=1,
 feb=2,february=2,
 mar=3,march=3,
 apr=4,april=4,
 may=5,
 jun=6,june=6,
 jul=7,july=7,
 aug=8,august=8,
 sep=9,september=9,sept=9,
 oct=10,october=10,
 nov=11,november=11,
 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月21日T14: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='BCE'
 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('&nbsp;',' '):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)
 -- Return date a filled, if necessary, with month and/or day from date b.
 -- The filled day is truncated to fit the number of days in the month.
 localfillmonth,fillday
 ifnota.monththen
 fillmonth=b.monthor1
 end
 ifnota.daythen
 fillday=b.dayor1
 end
 iffillmonthorfilldaythen-- need to create a new date
 a=Date(a,{
 month=fillmonth,
 day=math.min(filldayora.day,days_in_month(a.year,fillmonthora.month,a.calendar))
 })
 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'orcode=='M'orcode=='s'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
 elseifcode=='M'then
 ifS>=30then
 M=M+1
 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
 elseifcode=='M'orcode=='s'then
 M=hours*60+M
 ifcode=='M'then
 returnM
 end
 returnM*60+S
 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,
 }

AltStyle によって変換されたページ (->オリジナル) /