Module:Date
- Afrikaans
- Anarâškielâ
- अंगिका
- Аԥсшәа
- العربية
- Arpetan
- অসমীয়া
- Azərbaycanca
- تۆرکجه
- Basa Bali
- বাংলা
- 閩南語 / Bân-lâm-gí
- Беларуская
- Беларуская (тарашкевіца)
- भोजपुरी
- Bikol Central
- Български
- Bosanski
- Буряад
- Cebuano
- Corsu
- Cymraeg
- Dansk
- الدارجة
- Eesti
- Ελληνικά
- Español
- Esperanto
- فارسی
- Français
- Gaeilge
- Gĩkũyũ
- ગુજરાતી
- गोंयची कोंकणी / Gõychi Konknni
- 한국어
- Hausa
- Հայերեն
- हिन्दी
- Hrvatski
- Bahasa Hulontalo
- Ilokano
- Bahasa Indonesia
- Íslenska
- עברית
- Jawa
- Kabɩyɛ
- ಕನ್ನಡ
- ქართული
- Қазақша
- Ikinyarwanda
- Kiswahili
- Kreyòl ayisyen
- Kurdî
- ລາວ
- Latviešu
- Lëtzebuergesch
- Lietuvių
- Magyar
- मैथिली
- Македонски
- Malagasy
- മലയാളം
- Māori
- मराठी
- მარგალური
- ဘာသာမန်
- مازِرونی
- Bahasa Melayu
- Mfantse
- Монгол
- မြန်မာဘာသာ
- नेपाली
- 日本語
- Нохчийн
- Norsk bokmål
- Norsk nynorsk
- ଓଡ଼ିଆ
- Oʻzbekcha / ўзбекча
- ਪੰਜਾਬੀ
- Pangcah
- ပအိုဝ်ႏဘာႏသာႏ
- Papiamentu
- پښتو
- Português
- Română
- Русиньскый
- Русский
- سرائیکی
- Scots
- Setswana
- Shqip
- සිංහල
- Simple English
- سنڌي
- SiSwati
- Slovenščina
- کوردی
- Српски / srpski
- Srpskohrvatski / српскохрватски
- Suomi
- Tagalog
- தமிழ்
- Taqbaylit
- တႆး
- తెలుగు
- ไทย
- ትግርኛ
- Тоҷикӣ
- ತುಳು
- Türkçe
- Türkmençe
- Tyap
- Українська
- اردو
- Vahcuengh
- Vèneto
- Tiếng Việt
- Winaray
- 吴语
- ייִדיש
- Yorùbá
- 粵語
- Zazaki
- 中文
- Igala
- ᥖᥭᥰ ᥖᥬᥲ ᥑᥨᥒᥰ
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:
- 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')._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 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日 BC text=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月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 |
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' ')..(value<12andap[1]orap[2]) end end ifcode.field=='year'then localsign=(era_text[eraopt]or{}).sign ifnotsignorformat:find('%{era}',1,true)then sign='' ifvalue<=0then value=1-value end else ifvalue>=0then sign='' else value=-value end end returnspaces..sign..sformat(fmt,value) end returnspaces..(fmtandsformat(fmt,value)orvalue) end end localfunctionreplace_property(spaces,id) ifid=='era'then -- Special case so can use local era option. localresult=get_era_for_year(eraopt,date.year) ifresult==''then return'' end return(spaces==''and''or' ')..result end localresult=date[id] iftype(result)=='string'then returnspaces..result end iftype(result)=='number'then returnspaces..tostring(result) end iftype(result)=='boolean'then returnspaces..(resultand'1'or'0') end -- This occurs if id is an undefined field in a partial date, or is the name of a function. returnnil end localPERCENT='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(' ',' '):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, }