Module:Date
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:
- Module:Date/example • Demonstration showing how Module:Date may be used.
- Module talk:Date/example • Output from the demonstration.
Formatted output
A date can be formatted as text.
localDate=require('Module:Date')._Datelocaltext=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 -- ResultDate=require('Module:Date')._Datedate=Date('juliandate',320)number=date.gsd-- -1721105number=date.jd-- 320text=date.dayname-- Saturdaytext=date:text()-- 9 October 4713 BCtext=date:text('%Y-%m-%d')-- 4713-10-09text=date:text('%{era} %Y-%m-%d')-- BC 4713-10-09text=date:text('%Y-%m-%d %{era}')-- 4713-10-09 BCtext=date:text('%Y-%m-%d %{era}','era=B.C.E.')-- 4713-10-09 B.C.E.text=date:text('%Y-%m-%d','era=BCNEGATIVE')-- -4712-10-09text=date:text('%Y-%m-%d','era=BCMINUS')-- −4712-10-09 (uses Unicode MINUS SIGN U+2212)text=Date('juliandate',320):text('%{gsd} %{jd}')-- -1721105 320text=Date('Oct 9, 4713 B.C.E.'):text('%{gsd} %{jd}')-- -1721105 320text=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 -- ResultDate=require('Module:Date')._Datedate1=Date('21 Mar 2015')date2=Date('4 Dec 1999')diff=date1-date2d=diff.age_days-- 5586y,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, 17y,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, 4d=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-date2neg=diff.isnegative-- falsetext=diff.date1:text()-- 21 March 2015text=diff.date2:text()-- 4 December 1999diff=date2-date1neg=diff.isnegative-- true (dates have been swapped)text=diff.date1:text()-- 21 March 2015text=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-date2y,m,d=diff.years,diff.months,diff.days-- 2, 1, 17H,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-date2date3=date2+diffdate4=date1-difftext=date3:text('ymd hms')-- 2016-03-08 00:30:45text=date4:text('ymd hms')-- 2014-01-19 22:55:00equal=(date1==date3)-- trueequal=(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 |
-- Date functions for use by other modules.-- I18N and time zones are not supported.localMINUS='−'-- Unicode U+2212 MINUS SIGNlocalfloor=math.floorlocalDate,DateDiff,diffmt-- forward declarationslocaluniq={'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==uniqendlocalfunctionis_diff(t)returntype(t)=='table'andgetmetatable(t)==diffmtendlocalfunction_list_join(list,sep)returntable.concat(list,sep)endlocalfunctioncollection()-- Return a table to hold items.return{n=0,add=function(self,item)self.n=self.n+1self[self.n]=itemend,join=_list_join,}endlocalfunctionstrip_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'thentext=text:match('(%S.-)%s*$')endreturntextendlocalfunctionis_leap_year(year,calname)-- Return true if year is a leap year.ifcalname=='Julian'thenreturnyear%4==0endreturn(year%4==0andyear%100~=0)oryear%400==0endlocalfunctiondays_in_month(year,month,calname)-- Return number of days (1..31) in given month (1..12).ifmonth==2andis_leap_year(year,calname)thenreturn29endreturn({31,28,31,30,31,30,31,31,30,31,30,31})[month]endlocalfunctionh_m_s(time)-- Return hour, minute, second extracted from fraction of a day.time=floor(time*24*3600+0.5)-- number of secondslocalsecond=time%60time=floor(time/60)returnfloor(time/60),time%60,secondendlocalfunctionhms(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)/24endlocalfunctionjulian_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 calendarlocaloffsetlocala=floor((14-date.month)/12)localy=date.year+4800-aifdate.calendar=='Julian'thenoffset=floor(y/4)-32083elseoffset=floor(y/4)-floor(y/100)+floor(y/400)-32045endlocalm=date.month+12*a-3localjd=date.day+floor((153*m+2)/5)+365*y+offsetifdate.hastimethenjd=jd+hms(date)-0.5returnjd,jdendreturnjd,jd-0.5endlocalfunctionset_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.calendarlocallow,high-- min/max limits for date ranges −9999-01-01 to 9999-12-31ifcalname=='Gregorian'thenlow,high=-1930999.5,5373484.49999elseifcalname=='Julian'thenlow,high=-1931076.5,5373557.49999elsereturnendlocaljd=date.jdifnot(type(jd)=='number'andlow<=jdandjd<=high)thenreturnendlocaljdn=floor(jd)ifdate.hastimethenlocaltime=jd-jdn-- 0 <= time < 1iftime>=0.5then-- if at or after midnight of next dayjdn=jdn+1time=time-0.5elsetime=time+0.5enddate.hour,date.minute,date.second=h_m_s(time)elsedate.second=0date.minute=0date.hour=0endlocalb,cifcalname=='Julian'thenb=0c=jdn+32082else-- Gregorianlocala=jdn+32044b=floor((4*a+3)/146097)c=a-floor(146097*b/4)endlocald=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)+1date.month=m+3-12*floor(m/10)date.year=100*b+d-4800+floor(m/10)returntrueendlocalfunctionfix_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.localdateifnot(1<=mandm<=12)thendate=Date(y,1,1)ifnotdatethenreturnenddate=date+((m-1)..'m')y,m=date.year,date.monthendlocaldays_hmsifnotpartialthenifhastimeandHandMandSthenifnot(0<=HandH<=23and0<=MandM<=59and0<=SandS<=59)thendays_hms=hms({hour=H,minute=M,second=S})endendifdays_hmsornot(1<=dandd<=days_in_month(y,m,calendar))thendate=dateorDate(y,m,1)ifnotdatethenreturnenddate=date+(d-1+(days_hmsor0))y,m,d=date.year,date.month,date.dayifdays_hmsthenH,M,S=date.hour,date.minute,date.secondendendendnumbers.year=ynumbers.month=mnumbers.day=difdays_hmsthen-- Don't set H unless it was valid because a valid H will set hastime.numbers.hour=Hnumbers.minute=Mnumbers.second=Sendendlocalfunctionset_date_from_numbers(date,numbers,options)-- Set the fields of table date from numeric values.-- Return true if date is valid.iftype(numbers)~='table'thenreturnendlocaly=numbers.yearordate.yearlocalm=numbers.monthordate.monthlocald=numbers.dayordate.daylocalH=numbers.hourlocalM=numbers.minuteordate.minuteor0localS=numbers.secondordate.secondor0localneed_fixifyandmanddthendate.partial=nilifnot(-9999<=yandy<=9999and1<=mandm<=12and1<=dandd<=days_in_month(y,m,date.calendar))thenifnotdate.want_fixthenreturnendneed_fix=trueendelseifyanddate.partialthenifdornot(-9999<=yandy<=9999)thenreturnendifmandnot(1<=mandm<=12)thenifnotdate.want_fixthenreturnendneed_fix=trueendelsereturnendifdate.partialthenH=nil-- ignore any timeM=nilS=nilelseifHthen-- It is not possible to set M or S without also setting H.date.hastime=trueelseH=0endifnot(0<=HandH<=23and0<=MandM<=59and0<=SandS<=59)thenifdate.want_fixthenneed_fix=trueelsereturnendendenddate.want_fix=nilifneed_fixthenfix_numbers(numbers,y,m,d,H,M,S,date.partial,date.hastime,date.calendar)returnset_date_from_numbers(date,numbers,options)enddate.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'thenfor_,kinipairs({'am','era','format'})doifoptions[k]thendate.options[k]=options[k]endendendreturntrueendlocalfunctionmake_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'thenresult.am=options1.amresult.era=options1.eraelseiftype(options1)=='string'then-- Example: 'am:AM era:BC' or 'am=AM era=BC'.foriteminoptions1:gmatch('%S+')dolocallhs,rhs=item:match('^(%w+)[:=](.+)$')iflhsthenresult[lhs]=rhsendendendoptions2=type(options2)=='table'andoptions2or{}localdefaults={am='am',era='BC'}fork,vinpairs(defaults)doifnotresult[k]thenifoptions2[k]thenresult[k]=options2[k]elseresult[k]=vresult.bydefault[k]=trueendendendreturnresultendlocalampm_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''endlocalfunctionstrftime(date,format,options)-- Return date formatted as a string using codes similar to those-- in the C strftime library function.localsformat=string.formatlocalshortcuts={['%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]thenformat=shortcuts[format]endlocalcodes={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.amlocaleraopt=options.eralocalfunctionreplace_code(spaces,modifier,id)localcode=codes[id]ifcodethenlocalfmt=code.fmtifmodifier=='-'andcode.fmt2thenfmt=code.fmt2endlocalvalue=date[code.field]ifnotvaluethenreturnnil-- an undefined field in a partial dateendlocalspecial=code.specialifspecialthenifspecial=='hour12'thenvalue=value%12value=value==0and12orvalueelseifspecial=='am'thenlocalap=({['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])endendifcode.field=='year'thenlocalsign=(era_text[eraopt]or{}).signifnotsignorformat:find('%{era}',1,true)thensign=''ifvalue<=0thenvalue=1-valueendelseifvalue>=0thensign=''elsevalue=-valueendendreturnspaces..sign..sformat(fmt,value)endreturnspaces..(fmtandsformat(fmt,value)orvalue)endendlocalfunctionreplace_property(spaces,id)ifid=='era'then-- Special case so can use local era option.localresult=get_era_for_year(eraopt,date.year)ifresult==''thenreturn''endreturn(spaces==''and''or' ')..resultendlocalresult=date[id]iftype(result)=='string'thenreturnspaces..resultendiftype(result)=='number'thenreturnspaces..tostring(result)endiftype(result)=='boolean'thenreturnspaces..(resultand'1'or'0')end-- This occurs if id is an undefined field in a partial date, or is the name of a function.returnnilendlocalPERCENT='\127PERCENT\127'return(format:gsub('%%%%',PERCENT):gsub('(%s*)%%{(%w+)}',replace_property):gsub('(%s*)%%(%-?)(%a)',replace_code):gsub(PERCENT,'%%'))endlocalfunction_date_text(date,fmt,options)-- Return a formatted string representing the given date.ifnotis_date(date)thenerror('date:text: need a date (use "date:text()" with a colon)',2)endiftype(fmt)=='string'andfmt:match('%S')theniffmt:find('%',1,true)thenreturnstrftime(date,fmt,options)endelseifdate.partialthenfmt=date.monthand'my'or'y'elsefmt='dmy'ifdate.hastimethenfmt=(date.second>0and'hms 'or'hm ')..fmtendendlocalfunctionbad_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)endifdate.partialthen-- Ignore days in standard formats like 'ymd'.iffmt=='ym'orfmt=='ymd'thenfmt=date.monthand'%Y-%m %{era}'or'%Y %{era}'elseiffmt=='my'orfmt=='dmy'orfmt=='mdy'thenfmt=date.monthand'%B %-Y %{era}'or'%-Y %{era}'elseiffmt=='y'thenfmt=date.monthand'%-Y %{era}'or'%-Y %{era}'elsereturnbad_format()endreturnstrftime(date,fmt,options)endlocalfunctionhm_fmt()localplain=make_option_table(options,date.options).bydefault.amreturnplainand'%H:%M'or'%-I:%M %p'endlocalneed_time=date.hastimelocalt=collection()foriteminfmt:gmatch('%S+')dolocalfifitem=='hm'thenf=hm_fmt()need_time=falseelseifitem=='hms'thenf='%H:%M:%S'need_time=falseelseifitem=='ymd'thenf='%Y-%m-%d %{era}'elseifitem=='mdy'thenf='%B %-d, %-Y %{era}'elseifitem=='dmy'thenf='%-d %B %-Y %{era}'elsereturnbad_format()endt:add(f)endfmt=t:join(' ')ifneed_timethenfmt=hm_fmt()..' '..fmtendreturnstrftime(date,fmt,options)endlocalday_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'thenreturntranslate[text:lower()]endendlocalfunctionday_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,})endlocalfunctionmonth_number(text)returnname_to_number(text,{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,})endlocalfunction_list_text(list,fmt)-- Return a list of formatted strings from a list of dates.ifnottype(list)=='table'thenerror('date:list:text: need "list:text()" with a colon',2)endlocalresult={join=_list_join}fori,dateinipairs(list)doresult[i]=date:text(fmt)endreturnresultendlocalfunction_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)thenerror('date:list: need a date (use "date:list()" with a colon)',2)endlocallist={text=_list_text}ifdate.partialthenreturnlistendlocalcount,offset,operationlocalops={['>=']={before=false,include=true},['>']={before=false,include=false},['<=']={before=true,include=true},['<']={before=true,include=false},}ifspectheniftype(spec)=='number'thencount=floor(spec+0.5)ifcount<0thencount=-countoperation=ops['<']endelseiftype(spec)=='string'thenlocalnum,day,op=spec:match('^%s*(%d*)%s*(%a*)%s*([<>=]*)%s*$')ifnotnumthenreturnlistendifnum~=''thencount=tonumber(num)endifday~=''thenlocaldow=day_number(day:gsub('[sS]$',''))-- accept plural daysifnotdowthenreturnlistendoffset=dow-date.dowendoperation=ops[op]elsereturnlistendendoffset=offsetor0operation=operationorops['>']localdatefrom,dayfirst,daylastifoperation.beforethenifoffset>0or(offset==0andnotoperation.include)thenoffset=offset-7endifcountthenifcount>1thenoffset=offset-7*(count-1)enddatefrom=date+offsetelsedaylast=date.day+offsetdayfirst=daylast%7ifdayfirst==0thendayfirst=7endendelseifoffset<0or(offset==0andnotoperation.include)thenoffset=offset+7endifcountthendatefrom=date+offsetelsedayfirst=date.day+offsetdaylast=date.monthdaysendendifnotcountthenifdaylast<dayfirstthenreturnlistendcount=floor((daylast-dayfirst)/7)+1datefrom=Date(date,{day=dayfirst})endfori=1,countdoifnotdatefromthenbreakend-- exceeds date limitslist[i]=datefromdatefrom=datefrom+7endreturnlistend-- 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.yearself.month=d.monthself.day=d.dayself.hour=d.hourself.minute=d.minself.second=d.secreturnrawget(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$')ifsigntheny=tonumber(y)ifsign=='-'andy>0theny=-yendify<=0thenoptions.era='BCE'enddate.year=ym=tonumber(m)d=tonumber(d)H=tonumber(H)M=tonumber(M)S=tonumber(S)ifm==0thennewdate.partial=truereturndate,optionsenddate.month=mifd==0thennewdate.partial=truereturndate,optionsenddate.day=difH>0orM>0orS>0thendate.hour=Hdate.minute=Mdate.second=Sendreturndate,optionsendreturnendlocalfunctionextract_ymd(item)-- Called when no day or month has been set.localy,m,d=item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')ifythenifdate.yearthenreturnendifm:match('^%d%d?$')thenm=tonumber(m)elsem=month_number(m)endifmthendate.year=tonumber(y)date.month=mdate.day=tonumber(d)returntrueendendendlocalfunctionextract_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?)(.*)$')ifnumberthenlocaln=tonumber(number)if#number<=2andn<=31thensuffix=suffix:lower()ifsuffix==''orsuffix=='st'orsuffix=='nd'orsuffix=='rd'orsuffix=='th'thendate.day=nreturntrueendelseifsuffix==''andnewdate.partialandnotdate.yearthendate.year=nreturntrueendendendlocalfunctionextract_month(item)-- A month must be given as a name or abbreviation; a number could be ambiguous.localm=month_number(item)ifmthendate.month=mreturntrueendendlocalfunctionextract_time(item)localh,m,s=item:match('^(%d%d?):(%d%d)(:?%d*)$')ifdate.hourornoththenreturnendifs~=''thens=s:match('^:(%d%d)$')ifnotsthenreturnendenddate.hour=tonumber(h)date.minute=tonumber(m)date.second=tonumber(s)-- nil if empty stringreturntrueendlocalitem_count=0localindex_timelocalfunctionset_ampm(item)localH=date.hourifHandnotoptions.amandindex_time+1==item_countthenoptions.am=ampm_options[item]-- caller checked this is not nilifitem:match('^[Aa]')thenifnot(1<=HandH<=12)thenreturnendifH==12thendate.hour=0endelseifnot(1<=HandH<=23)thenreturnendifH<=11thendate.hour=H+12endendreturntrueendendforitemintext:gsub(',',' '):gsub(' ',' '):gmatch('%S+')doitem_count=item_count+1ifera_text[item]then-- Era is accepted in peculiar places.ifoptions.erathenreturnendoptions.era=itemelseifampm_options[item]thenifnotset_ampm(item)thenreturnendelseifitem:find(':',1,true)thenifnotextract_time(item)thenreturnendindex_time=item_countelseifdate.dayanddate.monththenifdate.yearthenreturn-- should be nothing more so item is invalidendifnotitem:match('^(%d%d?%d?%d?)$')thenreturnenddate.year=tonumber(item)elseifdate.daythenifnotextract_month(item)thenreturnendelseifdate.monththenifnotextract_day_or_year(item)thenreturnendelseifextract_month(item)thenoptions.format='mdy'elseifextract_ymd(item)thenoptions.format='ymd'elseifextract_day_or_year(item)thenifdate.daythenoptions.format='dmy'endelsereturnendendifnotdate.yearordate.year==0thenreturnendlocalera=era_text[options.era]iferaandera.isbcthendate.year=1-date.yearendreturndate,optionsendlocalfunctionautofill(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,filldayifnota.monththenfillmonth=b.monthor1endifnota.daythenfillday=b.dayor1endiffillmonthorfilldaythen-- need to create a new datea=Date(a,{month=fillmonth,day=math.min(filldayora.day,days_in_month(a.year,fillmonthora.month,a.calendar))})endreturnaendreturnfilled(date1,date2),filled(date2,date1)endlocalfunctiondate_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.returnendlocalfunctionis_prefix(text,word,minlen)localn=#textreturn(minlenor1)<=nandn<=#wordandtext==word:sub(1,n)endlocalfunctiondo_days(n)localforcetime,jdiffloor(n)==nthenjd=lhs.jdelseforcetime=notlhs.hastimejd=lhs.jdzendjd=jd+(is_suband-norn)ifforcetimethenjd=tostring(jd)ifnotjd:find('.',1,true)thenjd=jd..'.0'endendreturnDate(lhs,'juliandate',jd)endiftype(rhs)=='number'then-- Add/subtract days, including fractional days.returndo_days(rhs)endiftype(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+)$')ifsignthenifsign=='-'thenis_sub=not(is_subandtrueorfalse)endlocaly,m,dayslocalnum=tonumber(numstr)ifnotnumthenreturnendid=id:lower()ifis_prefix(id,'years')theny=numm=0elseifis_prefix(id,'months')theny=floor(num/12)m=num%12elseifis_prefix(id,'weeks')thendays=num*7elseifis_prefix(id,'days')thendays=numelseifis_prefix(id,'hours')thendays=num/24elseifis_prefix(id,'minutes',3)thendays=num/(24*60)elseifis_prefix(id,'seconds')thendays=num/(24*3600)elsereturnendifdaysthenreturndo_days(days)endifnumstr:find('.',1,true)thenreturnendifis_subtheny=-ym=-mendassert(-11<=mandm<=11)y=lhs.year+ym=lhs.month+mifm>12theny=y+1m=m-12elseifm<1theny=y-1m=m+12endlocald=math.min(lhs.day,days_in_month(y,m,lhs.calendar))returnDate(lhs,y,m,d)endendifis_diff(rhs)thenlocaldays=rhs.age_daysif(is_suborfalse)~=(rhs.isnegativeorfalse)thendays=-daysendreturnlhs+daysendendlocalfull_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')theniffull_date_only[key]thenreturnendifkey=='monthabbr'orkey=='monthdays'orkey=='monthname'thenifnotself.monththenreturnendendendlocalvalueifkey=='dayabbr'thenvalue=day_info[self.dow][1]elseifkey=='dayname'thenvalue=day_info[self.dow][2]elseifkey=='dow'thenvalue=(self.jdnoon+1)%7-- day-of-week 0=Sun to 6=Satelseifkey=='dayofweek'thenvalue=self.dowelseifkey=='dowiso'thenvalue=(self.jdnoon%7)+1-- ISO day-of-week 1=Mon to 7=Sunelseifkey=='dayofweekiso'thenvalue=self.dowisoelseifkey=='dayofyear'thenlocalfirst=Date(self.year,1,1,self.calendar).jdnoonvalue=self.jdnoon-first+1-- day-of-year 1 to 366elseifkey=='era'then-- Era text (never a negative sign) from year and options.value=get_era_for_year(self.options.era,self.year)elseifkey=='format'thenvalue=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'thenlocaljd,jdz=julian_date(self)rawset(self,'juliandate',jd)rawset(self,'jd',jd)rawset(self,'jdz',jdz)returnkey=='jdz'andjdzorjdelseifkey=='jdnoon'then-- Julian date at noon (an integer) on the calendar day when jd occurs.value=floor(self.jd+0.5)elseifkey=='isleapyear'thenvalue=is_leap_year(self.year,self.calendar)elseifkey=='monthabbr'thenvalue=month_info[self.month][1]elseifkey=='monthdays'thenvalue=days_in_month(self.year,self.month,self.calendar)elseifkey=='monthname'thenvalue=month_info[self.month][2]endifvalue~=nilthenrawset(self,key,value)returnvalueendend,}-- Date operators.localfunctionmt_date_add(lhs,rhs)ifnotis_date(lhs)thenlhs,rhs=rhs,lhs-- put date on left (it must be a date for this to have been called)endreturndate_add_sub(lhs,rhs)endlocalfunctionmt_date_sub(lhs,rhs)ifis_date(lhs)thenifis_date(rhs)thenreturnDateDiff(lhs,rhs)endreturndate_add_sub(lhs,rhs,true)endendlocalfunctionmt_date_concat(lhs,rhs)returntostring(lhs)..tostring(rhs)endlocalfunctionmt_date_tostring(self)returnself:text()endlocalfunctionmt_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.calendarendreturnlhs.jdz==rhs.jdzendlocalfunctionmt_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.calendarthenreturnlhs.calendar=='Julian'endiflhs.partialthenlhs=lhs.partial.firstendifrhs.partialthenrhs=rhs.partial.firstendendreturnlhs.jdz<rhs.jdzend--[[ 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:SDate('currentdate')Date('currentdatetime')Date('1 April 1995', 'julian') parse date from textDate('1 April 1995 AD', 'julian') using an era sets a flag to do the same for outputDate('04:30:59 1 April 1995', 'julian')Date(date) copy of an existing dateDate(date, t) same, updated with y,m,d,H,M,S fields from table tDate(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 calendarhastime=false,-- true if input sets a timehour=0,-- always set hour/minute/second so don't have to handle nilminute=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,tnumslocalnumindex=0localnumfields={'year','month','day','hour','minute','second'}localnumbers={}for_,vinipairs({...})dov=strip_to_nil(v)localvlower=type(v)=='string'andv:lower()ornilifv==nilthen-- Ignore empty arguments after stripping so modules can directly pass template parameters.elseifcalendars[vlower]thennewdate.calendar=calendars[vlower]elseifvlower=='partial'thennewdate.partial=trueelseifvlower=='fix'thennewdate.want_fix=trueelseifis_date(v)then-- Copy existing date (items can be overridden by other arguments).ifis_copyortnumsthenreturnendis_copy=truenewdate.calendar=v.calendarnewdate.partial=v.partialnewdate.hastime=v.hastimenewdate.options=v.optionsnewdate.year=v.yearnewdate.month=v.monthnewdate.day=v.daynewdate.hour=v.hournewdate.minute=v.minutenewdate.second=v.secondelseiftype(v)=='table'theniftnumsthenreturnendtnums={}localtfields={year=1,month=1,day=1,hour=2,minute=2,second=2}fortk,tvinpairs(v)doiftfields[tk]thentnums[tk]=tonumber(tv)endiftfields[tk]==2thennewdate.hastime=trueendendelselocalnum=tonumber(v)ifnotnumandargtype=='setdate'andnumindex==1thennum=month_number(v)endifnumthenifnotargtypethenargtype='setdate'endifargtype=='setdate'andnumindex<6thennumindex=numindex+1numbers[numfields[numindex]]=numelseifargtype=='juliandate'andnotjd_numberthenjd_number=numiftype(v)=='string'thenifv:find('.',1,true)thennewdate.hastime=trueendelseifnum~=floor(num)then-- The given value was a number. The time will be used-- if the fractional part is nonzero.newdate.hastime=trueendelsereturnendelseifargtypethenreturnelseiftype(v)=='string'thenifv=='currentdate'orv=='currentdatetime'orv=='juliandate'thenargtype=velseargtype='datetext'datetext=vendelsereturnendendendifargtype=='datetext'theniftnumsornotset_date_from_numbers(newdate,extract_date(newdate,datetext))thenreturnendelseifargtype=='juliandate'thennewdate.partial=nilnewdate.jd=jd_numberifnotset_date_from_jd(newdate)thenreturnendelseifargtype=='currentdate'orargtype=='currentdatetime'thennewdate.partial=nilnewdate.year=current.yearnewdate.month=current.monthnewdate.day=current.dayifargtype=='currentdatetime'thennewdate.hour=current.hournewdate.minute=current.minutenewdate.second=current.secondnewdate.hastime=trueendnewdate.calendar='Gregorian'-- ignore any given calendar nameelseifargtype=='setdate'theniftnumsornotset_date_from_numbers(newdate,numbers)thenreturnendelseifnot(is_copyortnums)thenreturnendiftnumsthennewdate.jd=nil-- force recalculation in case jd was set before changes from tnumsifnotset_date_from_numbers(newdate,tnums)thenreturnendendifnewdate.partialthenlocalyear=newdate.yearlocalmonth=newdate.monthlocalfirst=Date(year,monthor1,1,newdate.calendar)month=monthor12locallast=Date(year,month,days_in_month(year,month),newdate.calendar)newdate.partial={first=first,last=last}elsenewdate.partial=false-- avoid index lookupendsetmetatable(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)endlocalfunction_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,wantrangeiftype(options)=='table'thenwantround=options.roundwantduration=options.durationwantrange=options.rangeelsewantround=optionsendifnotis_diff(diff)thenlocalf=wantdurationand'duration'or'age'error(f..': need a date difference (use "diff:'..f..'()" with a colon)',2)endifdiff.partialthen-- Ignore wantround, wantduration.localfunctionchoose(v)iftype(v)=='table'thenifnotwantrangeorv[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]endendreturnvendifcode=='ym'orcode=='ymd'thenifnotwantrangeanddiff.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,0endreturnchoose(diff.partial.years),choose(diff.partial.months)endifcode=='y'thenreturnchoose(diff.partial.years)endifcode=='m'orcode=='w'orcode=='d'thenreturnchoose({diff.partial.mindiff:age(code),diff.partial.maxdiff:age(code)})endreturnnilendlocalextra_days=wantdurationand1or0ifcode=='wd'orcode=='w'orcode=='d'thenlocaloffset=wantroundand0.5or0localdays=diff.age_days+extra_daysifcode=='wd'orcode=='d'thendays=floor(days+offset)ifcode=='d'thenreturndaysendreturnfloor(days/7),days%7endreturnfloor(days/7+offset)endlocalH,M,S=diff.hours,diff.minutes,diff.secondsifcode=='dh'orcode=='dhm'orcode=='dhms'orcode=='h'orcode=='hm'orcode=='hms'orcode=='M'orcode=='s'thenlocaldays=floor(diff.age_days+extra_days)localinc_hourifwantroundthenifcode=='dh'orcode=='h'thenifM>=30theninc_hour=trueendelseifcode=='dhm'orcode=='hm'thenifS>=30thenM=M+1ifM>=60thenM=0inc_hour=trueendendelseifcode=='M'thenifS>=30thenM=M+1endelse-- Nothing needed because S is an integer.endifinc_hourthenH=H+1ifH>=24thenH=0days=days+1endendendifcode=='dh'orcode=='dhm'orcode=='dhms'thenifcode=='dh'thenreturndays,Helseifcode=='dhm'thenreturndays,H,Melsereturndays,H,M,Sendendlocalhours=days*24+Hifcode=='h'thenreturnhourselseifcode=='hm'thenreturnhours,Melseifcode=='M'orcode=='s'thenM=hours*60+Mifcode=='M'thenreturnMendreturnM*60+Sendreturnhours,M,Sendifwantroundthenlocalinc_hourifcode=='ymdh'orcode=='ymwdh'thenifM>=30theninc_hour=trueendelseifcode=='ymdhm'orcode=='ymwdhm'thenifS>=30thenM=M+1ifM>=60thenM=0inc_hour=trueendendelseifcode=='ymd'orcode=='ymwd'orcode=='yd'orcode=='md'thenifH>=12thenextra_days=extra_days+1endendifinc_hourthenH=H+1ifH>=24thenH=0extra_days=extra_days+1endendendlocaly,m,d=diff.years,diff.months,diff.daysifextra_days>0thend=d+extra_daysifd>28orcode=='yd'then-- Recalculate in case have passed a month.diff=diff.date1+extra_days-diff.date2y,m,d=diff.years,diff.months,diff.daysendendifcode=='ymd'thenreturny,m,delseifcode=='yd'thenify>0then-- It is known that diff.date1 > diff.date2.diff=diff.date1-(diff.date2+(y..'y'))endreturny,floor(diff.age_days)elseifcode=='md'thenreturny*12+m,delseifcode=='ym'orcode=='m'thenifwantroundthenifd>=16thenm=m+1ifm>=12thenm=0y=y+1endendendifcode=='ym'thenreturny,mendreturny*12+melseifcode=='ymw'thenlocalweeks=floor(d/7)ifwantroundthenlocaldays=d%7ifdays>3or(days==3andH>=12)thenweeks=weeks+1endendreturny,m,weekselseifcode=='ymwd'thenreturny,m,floor(d/7),d%7elseifcode=='ymdh'thenreturny,m,d,Helseifcode=='ymwdh'thenreturny,m,floor(d/7),d%7,Helseifcode=='ymdhm'thenreturny,m,d,H,Melseifcode=='ymwdhm'thenreturny,m,floor(d/7),d%7,H,Mendifcode=='y'thenifwantroundandm>=6theny=y+1endreturnyendreturnnilendlocalfunction_diff_duration(diff,code,options)iftype(options)~='table'thenoptions={round=options}endoptions.duration=truereturn_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)localvalueifkey=='age_days'thenifrawget(self,'partial')thenlocalfunctionjdz(date)return(date.partialanddate.partial.firstordate).jdzendvalue=jdz(self.date1)-jdz(self.date2)elsevalue=self.date1.jdz-self.date2.jdzendendifvalue~=nilthenrawset(self,key,value)returnvalueendend,}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)thenreturnendlocalwantfilliftype(options)=='table'thenwantfill=options.fillendlocalisnegative=falselocaliszero=falseifdate1<date2thenisnegative=truedate1,date2=date2,date1elseifdate1==date2theniszero=trueend-- 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-31ifwantfillthendate1,date2=autofill(date1,date2)elselocalfunctionzdiff(date1,date2)localdiff=date1-date2ifdiff.isnegativethenreturndate1-date1-- a valid diff in case we call its methodsendreturndiffendlocalfunctiongetdate(date,which)returndate.partialanddate.partial[which]ordateendlocalmaxdiff=zdiff(getdate(date1,'last'),getdate(date2,'first'))localmindiff=zdiff(getdate(date1,'first'),getdate(date2,'last'))localyears,monthsifmaxdiff.years==mindiff.yearsthenyears=maxdiff.yearsifmaxdiff.months==mindiff.monthsthenmonths=maxdiff.monthselsemonths={mindiff.months,maxdiff.months}endelseyears={mindiff.years,maxdiff.years}endreturnsetmetatable({date1=date1,date2=date2,partial={years=years,months=months,maxdiff=maxdiff,mindiff=mindiff,},isnegative=isnegative,iszero=iszero,age=_diff_age,duration=_diff_duration,},diffmt)endendlocaly1,m1=date1.year,date1.monthlocaly2,m2=date2.year,date2.monthlocalyears=y1-y2localmonths=m1-m2locald1=date1.day+hms(date1)locald2=date2.day+hms(date2)localdays,timeifd1>=d2thendays=d1-d2elsemonths=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)or31ifd2>=dpmthendays=d1-hms(date2)elsedays=dpm-d2+d1endendifmonths<0thenyears=years-1months=months+12enddays,time=math.modf(days)localH,M,S=h_m_s(time)returnsetmetatable({date1=date1,date2=date2,partial=false,-- avoid index lookupyears=years,months=months,days=days,hours=H,minutes=M,seconds=S,isnegative=isnegative,iszero=iszero,age=_diff_age,duration=_diff_duration,},diffmt)endreturn{_current=current,_Date=Date,_days_in_month=days_in_month,}