Módulo:Date
Nota: Éste módulo es una traducción de Module:Date lo que significa que cualquier módulo que dependa de Module:Date se puede utilizar con éste módulo sin ningún problema (siempre que el módulo en cuestión se adapte para entender fechas en español si fuese necesario). Es por eso que a pesar de que las fechas que devuelve al utilizar :text()
son en español todos los nombres de los métodos internos y los parámetros se han mantenido en el idioma original.
Este módulo provee funciones relacionadas con el manejo de fechas para que puedan usarse en otros módulos. El módulo soporta fechas en el Calendario Gregoriano y en el calendario Juliano, desde el 9999 a. C. al 9999 d. C.. Los calendarios son prolépticos, es decir, se aplican incluso antes de su creación sin irregularidades.
Una fecha , con su hora opcional, se puede especificar en varios formatos diferentes y se puede formatear para su impresión usando otra variedad de formatos, por ejemplo '1 de abril de 2016' o el 'abril 1, 2016'. Las propiedades de la fecha incluyen su Fecha juliana y los días desde el 1 A.D. así como el día de la semana y el día del año.
También es posible comparar fechas (por ejemplo, fecha1 <= fecha2
), y se les pueden aplicar operadores como la suma o la resta (por ejemplo, fecha + '3 meses'
o fecha1 - fecha2
). Estas operaciones funcionan con fechas de los calendarios Juliano y Gregoriano pero será nil si intentas operar con dos fechas de diferentes calendarios.
El módulo provee los siguientes objetos.
Export | Descripción |
---|---|
_current |
Tabla con el año, mes, día, hora, minuto y segundo actuales. |
_Date |
Función que devuelve una tabla para una fecha concreta. |
_days_in_month |
Función que retorna el número de días en un mes. |
En Módulo:Date/ejemplos se encuentran los ejemplos de uso del módulo y en Módulo discusión:Date/ejemplos está el resultado de su ejecución.
Formato de la salida
Es posible formatear la representación textual de la fecha.
local Date = require('Módulo:Date')._Date
local text = Date(2016, 7, 1):text() -- devolvería '1 de julio de 2016'
local text = Date(2016, 7, 1):text('%-d de %B') -- devolvería '1 de julio'
local text = Date('1 de julio de 2016'):text('mdy') -- devolvería 'julio 1, 2016'
Los siguientes son los códigos de formato simplificados disponibles.
Código | Resultado |
---|---|
hm | horas:minutos, utilizando "am" o "pm" o una variante especificada (14:30 o 2:30 pm o lo especificado) |
hms | horas:minutos:segundos (14:30:45) |
ymd | año-mes-día (2016-07-01) |
mdy | mes día, año (julio 1, 2016) |
dmy | día de mes de año (1 de julio de 2016) |
También están disponibles los siguientes códigos están disponibles (similar a los utilizados por strftime).
Código | Resultado |
---|---|
%a | Abreviación del día: Lu, Ma, ... |
%A | Nombre del día: lunes, martes, ... |
%u | Día de la semana: de 1 a 7 (Lunes a Domingo) |
%w | Día de la semana: de 0 a 6 (Domingo a Sábado) |
%d | Día del mes relleno con ceros: 01 a 31 |
%b | Abreviación del mes: ene a dic |
%B | Nombre del mes: enero a diciembre |
%m | Mes relleno con ceros: 01 a 12 |
%Y | Año relleno con ceros: 0012, 0120, 1200 |
%H | Hora (reloj de 24 horas) rellena con ceros: 00 a 23 |
%I | Hora (reloj de 12 horas) rellena con ceros: 01 a 12 |
%p | AM o PM como con las opciones |
%M | Minutos rellenos con ceros: 00 a 59 |
%S | Segundos rellenos con ceros: 00 a 59 |
%j | Día del año relleno con ceros: 001 a 366 |
%-d | Día del mes: 1 a 31 |
%-m | Mes: 1 a 12 |
%-Y | Año: 12, 120, 1200 |
%-H | Hora: 0 a 23 |
%-M | Minutos: 0 a 59 |
%-S | Segundos: 0 a 59 |
%-j | Día del año: 1 a 366 |
%-I | Hora: 1 a 12 |
%% | % |
Además también se puede utilizar %{property}
(donde property
es una de las propiedades de la fecha).
Por ejemplo, una fecha como Date('1 de febrero de 2015 14:30:45 d. C.')
tiene las siguientes propiedades.
Código | Resultado |
---|---|
%{calendar} | Gregorian |
%{year} | 2015 |
%{month} | 2 |
%{day} | 1 |
%{hour} | 14 |
%{minute} | 30 |
%{second} | 45 |
%{dayabbr} | do |
%{dayname} | Domingo |
%{dayofweek} | 0 |
%{dow} | 0 (igual que 'dayofweek') |
%{dayofweekiso} | 7 |
%{dowiso} | 7 (igual que 'dayofweekiso') |
%{dayofyear} | 32 |
%{era} | d. C. |
%{gsd} | 735630 (números de días desde 1 de enero de 1 d. C.; el primero es el 1) |
%{juliandate} | 2457055.1046875 (Fecha juliana) |
%{jd} | 2457055.1046875 (igual que 'juliandate') |
%{isleapyear} | false |
%{monthdays} | 28 |
%{monthabbr} | feb |
%{monthname} | febrero |
También hay disponibles algunos atajos. Dada fecha = Date('1 feb 2015 14:30')
, obtendremos los siguientes resultados.
Código | Descripción | Resultado del ejemplo | Formato equivalente |
---|---|---|---|
fecha:text('%c') | fecha y hora | 2:30 pm 1 de febrero de 2015 | %-I:%M %p %-d %B %-Y %{era} |
fecha:text('%x') | fecha | 1 de febrero de 2015 | %-d %B %-Y %{era} |
fecha:text('%X') | hora | 2:30 pm | %-I:%M %p |
Fecha Juliana
El siguiente código contiene un ejemplo de la conversión de una Fecha juliana y la posterior obtención de información sobre esa fecha.
-- Código -- Resultado
Date = require('Módulo:Date')._Date
fecha = Date('juliandate', 320)
número = fecha.gsd -- -1721105
número = fecha.jd -- 320
texto = fecha.dayname -- sábado
texto = fecha:text() -- 9 de octubre de 4713 a. C.
texto = fecha:text('%Y-%m-%d') -- 4713-10-09
texto = fecha:text('%{era} %Y-%m-%d') -- a. C. 4713-10-09
texto = fecha:text('%Y-%m-%d %{era}') -- 4713-10-09 BC
texto = fecha:text('%Y-%m-%d %{era}', 'era=B.C.E.') -- 4713-10-09 B.C.E.
texto = fecha:text('%Y-%m-%d', 'era=BCNEGATIVE') -- -4712-10-09
texto = fecha:text('%Y-%m-%d', 'era=BCMINUS') -- −4712-10-09 (utiliza el símbolo menos de Unicode U+2212)
texto = Date('juliandate',320):text('%{gsd} %{jd}') -- -1721105 320
texto = Date('oct 9, 4713 B.C.E.'):text('%{gsd} %{jd}') -- -1721105 320
texto = Date(-4712,10,9):text('%{gsd} %{jd}') -- -1721105 320
Diferencia de fechas
La diferencia entre dos fechas se puede determinar utilizando fecha1 - fecha2
. El resultado es válido si las dos fechas utilizan el mismo calendario siendo 'nil' en otro caso. Es posible calcular una edad o una duración a partir de la diferencia entre dos fechas.
Por ejemplo:
-- Código -- Resultado
Date = require('Módulo:Date')._Date
fecha1 = Date('21 mar 2015')
fecha2 = Date('4 dic 1999')
diff = fecha1 - fecha2
d = diff.age_days -- 5586
y, m, d = diff.years, diff.months, diff.days -- 15, 3, 17 (15 años + 3 meses + 17 días)
y, m, d = diff:age('ymd') -- 15, 3, 17
y, m, w, d = diff:age('ymwd') -- 15, 3, 2, 3 (15 años + 3 meses + 2 semanas + 3 días)
y, m, w, d = diff:duration('ymwd') -- 15, 3, 2, 4
d = diff:duration('d') -- 5587 (duración en días incluyendo el último día)
Una diferencia de fechas mantiene las fechas originales pero están cambiadas de tal forma que diff.date1 >= diff.date2
(siendo siempre diff.date1
la más reciente). Esto se muestra a continuación.
fecha1 = Date('21 mar 2015')
fecha2 = Date('4 dic 1999')
diff = fecha1 - fecha2
neg = diff.isnegative -- false
text = diff.date1:text() -- 21 de marzo de 2015
text = diff.date2:text() -- 4 de diciembre de 1999
diff = fecha2 - fecha1
neg = diff.isnegative -- true (se han cambiado las fechas de orden)
text = diff.date1:text() -- 21 de marzo de 2015
text = diff.date2:text() -- 4 de diciembre de 1999
Una diferencia de fechas también guarda la diferencia de tiempo:
fecha1 = Date('8 mar 2016 0:30:45')
fecha2 = Date('19 ene 2014 22:55')
diff = fecha1 - fecha2
y, m, d = diff.years, diff.months, diff.days -- 2, 1, 17
H, M, S = diff.hours, diff.minutes, diff.seconds -- 1, 35, 45
Una diferencia de fechas también se puede añadir o restar a una fecha.
fecha1 = Date('8 mar 2016 0:30:45')
fecha2 = Date('19 ene 2014 22:55')
diff = fecha1 - fecha2
fecha3 = fecha2 + diff
fecha4 = fecha1 - diff
texto = date3:text('ymd hms') -- 2016-03-08 00:30:45
texto = date4:text('ymd hms') -- 2014-01-19 22:55:00
igualdad = (fecha1 == fecha3) -- true
igualdad = (fecha2 == fecha4) -- true
Los métodos de edad 'age' y duración 'duration' aceptan un código que identifica los componentes a retornar. En el caso de la duración se incluye un día extra (el último).
Código | Valores retornados |
---|---|
'ymwd' |
años, meses, semanas, días |
'ymd' |
años, meses, días |
'ym' |
años, meses |
'y' |
años |
'm' |
meses |
'wd' |
semanas, días |
'w' |
semanas |
'd' |
días |
Compatibilidad
Este módulo implementa las funciones de Módulo:Fecha y Módulo:Fechas, los tests de ambos módulos se encuentran funcionando contra este en Módulo:Date/tests y el resultado de su ejecución en Módulo discusión:Date/tests.
Error de secuencia de órdenes: Error de Lua: Error interno: El intérprete ha finalizado con la señal "-129".
-- Date functions for use by other modules. -- I18N and time zones are not supported. local MINUS = '−' -- Unicode U+2212 MINUS SIGN local floor = math.floor local Date, DateDiff, diffmt -- forward declarations local uniq = { 'unique identifier' } local function is_date(t) -- The system used to make a date read-only means there is no unique -- metatable that is conveniently accessible to check. return type(t) == 'table' and t._id == uniq end local function is_diff(t) return type(t) == 'table' and getmetatable(t) == diffmt end local function _list_join(list, sep) return table.concat(list, sep) end local function collection() -- 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 local function strip_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). if type(text) == 'string' then text = text:match('(%S.-)%s*$') end return text end local function is_leap_year(year, calname) -- Return true if year is a leap year. if calname == 'Julian' then return year % 4 == 0 end return (year % 4 == 0 and year % 100 ~= 0) or year % 400 == 0 end local function days_in_month(year, month, calname) -- Return number of days (1..31) in given month (1..12). if month == 2 and is_leap_year(year, calname) then return 29 end return ({ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 })[month] end local function h_m_s(time) -- Return hour, minute, second extracted from fraction of a day. time = floor(time * 24 * 3600 + 0.5) -- number of seconds local second = time % 60 time = floor(time / 60) return floor(time / 60), time % 60, second end local function hms(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 local function julian_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 local offset local a = floor((14 - date.month)/12) local y = date.year + 4800 - a if date.calendar == 'Julian' then offset = floor(y/4) - 32083 else offset = floor(y/4) - floor(y/100) + floor(y/400) - 32045 end local m = date.month + 12*a - 3 local jd = date.day + floor((153*m + 2)/5) + 365*y + offset if date.hastime then jd = jd + hms(date) - 0.5 return jd, jd end return jd, jd - 0.5 end local function set_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. local calname = date.calendar local low, high -- min/max limits for date ranges −9999-01-01 to 9999-12-31 if calname == 'Gregorian' then low, high = -1930999.5, 5373484.49999 elseif calname == 'Julian' then low, high = -1931076.5, 5373557.49999 else return end local jd = date.jd if not (type(jd) == 'number' and low <= jd and jd <= high) then return end local jdn = floor(jd) if date.hastime then local time = jd - jdn -- 0 <= time < 1 if time >= 0.5 then -- 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 local b, c if calname == 'Julian' then b = 0 c = jdn + 32082 else -- Gregorian local a = jdn + 32044 b = floor((4*a + 3)/146097) c = a - floor(146097*b/4) end local d = floor((4*c + 3)/1461) local e = c - floor(1461*d/4) local m = 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) return true end local function fix_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. local date if not (1 <= m and m <= 12) then date = Date(y, 1, 1) if not date then return end date = date + ((m - 1) .. 'm') y, m = date.year, date.month end local days_hms if not partial then if hastime and H and M and S then if not (0 <= H and H <= 23 and 0 <= M and M <= 59 and 0 <= S and S <= 59) then days_hms = hms({ hour = H, minute = M, second = S }) end end if days_hms or not (1 <= d and d <= days_in_month(y, m, calendar)) then date = date or Date(y, m, 1) if not date then return end date = date + (d - 1 + (days_hms or 0)) y, m, d = date.year, date.month, date.day if days_hms then H, M, S = date.hour, date.minute, date.second end end end numbers.year = y numbers.month = m numbers.day = d if days_hms then -- 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 local function set_date_from_numbers(date, numbers, options) -- Set the fields of table date from numeric values. -- Return true if date is valid. if type(numbers) ~= 'table' then return end local y = numbers.year or date.year local m = numbers.month or date.month local d = numbers.day or date.day local H = numbers.hour local M = numbers.minute or date.minute or 0 local S = numbers.second or date.second or 0 local need_fix if y and m and d then date.partial = nil if not (-9999 <= y and y <= 9999 and 1 <= m and m <= 12 and 1 <= d and d <= days_in_month(y, m, date.calendar)) then if not date.want_fix then return end need_fix = true end elseif y and date.partial then if d or not (-9999 <= y and y <= 9999) then return end if m and not (1 <= m and m <= 12) then if not date.want_fix then return end need_fix = true end else return end if date.partial then H = nil -- ignore any time M = nil S = nil else if H then -- It is not possible to set M or S without also setting H. date.hastime = true else H = 0 end if not (0 <= H and H <= 23 and 0 <= M and M <= 59 and 0 <= S and S <= 59) then if date.want_fix then need_fix = true else return end end end date.want_fix = nil if need_fix then fix_numbers(numbers, y, m, d, H, M, S, date.partial, date.hastime, date.calendar) return set_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 (*) if type(options) == 'table' then for _, k in ipairs({ 'am', 'era', 'format' }) do if options[k] then date.options[k] = options[k] end end end return true end local function make_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. local result = { bydefault = {} } if type(options1) == 'table' then result.am = options1.am result.era = options1.era elseif type(options1) == 'string' then -- Example: 'am:AM era:BC' or 'am=AM era=BC'. for item in options1:gmatch('%S+') do local lhs, rhs = item:match('^(%w+)[:=](.+)$') if lhs then result[lhs] = rhs end end end options2 = type(options2) == 'table' and options2 or {} local defaults = { am = 'am', era = 'a. C.' } for k, v in pairs(defaults) do if not result[k] then if options2[k] then result[k] = options2[k] else result[k] = v result.bydefault[k] = true end end end return result end local ampm_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.', } local era_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.' }, --ABREVIATURAS EN ESPAÑOL-- ['a. C.'] = { 'a. C.' , '' , isbc = true }, --antes de Cristo ['a. de C.'] = { 'a. de C.' , '' , isbc = true }, --antes de Cristo ['a. de J. C.'] = { 'a. de J. C.' , '' , isbc = true }, --antes de Jesucristo ['a. J. C.'] = { 'a. J. C.' , '' , isbc = true }, --antes de Jesucristo ['AEC'] = { 'AEC' , '' , isbc = true }, --antes de la era común ['a. e. c.'] = { 'a. e. c.' , '' , isbc = true }, --antes de la era común ['a. n. e.'] = { 'a. n. e.' , '' , isbc = true }, --antes de nuestra era ['a. e. v.'] = { 'a. e. v.' , '' , isbc = true }, --antes de la era vulgar ['d. C.'] = { 'a. C.' , 'd. C.' }, --después de Cristo ['d. de C.'] = { 'a. de C.' , 'd. de C.' }, --después de Cristo ['d. de J. C.'] = { 'a. de J. C.' , 'd. de J. C.' }, --después de Jesucristo ['d. J. C.'] = { 'a. J. C.' , 'd. J. C.' }, --después de Jesucristo ['EC'] = { 'AEC' , 'EC' }, --era común ['e. c.'] = { 'a. e. c.' , 'e. c.' }, --era común ['n. e.'] = { 'a. n. e.' , 'n. e.' }, --nuestra era ['e. v.'] = { 'a. e. v.' , 'e. v.' }, --era vulgar ['A. D.'] = { 'a. C.' , 'A. D.' }, --anno Domini } local function get_era_for_year(era, year) return (era_text[era] or era_text['a. C.'])[year > 0 and 2 or 1]:gsub(" ", " ") or '' end local function strftime(date, format, options) -- Return date formatted as a string using codes similar to those -- in the C strftime library function. local sformat = string.format local shortcuts = { ['%c'] = '%-I:%M %p %-d de %B de %-Y %{era}', -- date and time: 2:30 pm 1 de abril de 2016 ['%x'] = '%-d de %B de %-Y %{era}', -- date: 1 de abril de 2016 ['%X'] = '%-I:%M %p', -- time: 2:30 pm } if shortcuts[format] then format = shortcuts[format] end local codes = { 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) local amopt = options.am local eraopt = options.era local function replace_code(spaces, modifier, id) local code = codes[id] if code then local fmt = code.fmt if modifier == '-' and code.fmt2 then fmt = code.fmt2 end local value = date[code.field] if not value then return nil -- an undefined field in a partial date end local special = code.special if special then if special == 'hour12' then value = value % 12 value = value == 0 and 12 or value elseif special == 'am' then local ap = ({ ['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 < 12 and ap[1] or ap[2]) end end if code.field == 'year' then local sign = (era_text[eraopt] or {}).sign if not sign or format:find('%{era}', 1, true) then sign = '' if value <= 0 then value = 1 - value end else if value >= 0 then sign = '' else value = -value end end return spaces .. sign .. sformat(fmt, value) end return spaces .. (fmt and sformat(fmt, value) or value) end end local function replace_property(spaces, id) if id == 'era' then -- Special case so can use local era option. local result = get_era_for_year(eraopt, date.year) if result == '' then return '' end return (spaces == '' and '' or ' ') .. result end local result = date[id] if type(result) == 'string' then return spaces .. result end if type(result) == 'number' then return spaces .. tostring(result) end if type(result) == 'boolean' then return spaces .. (result and '1' or '0') end -- This occurs if id is an undefined field in a partial date, or is the name of a function. return nil end local PERCENT = '\127PERCENT\127' return (format :gsub('%%%%', PERCENT) :gsub('(%s*)%%{(%w+)}', replace_property) :gsub('(%s*)%%(%-?)(%a)', replace_code) :gsub(PERCENT, '%%') ) end local function _date_text(date, fmt, options) -- Return a formatted string representing the given date. if not is_date(date) then error('date:text: need a date (use "date:text()" with a colon)', 2) end if type(fmt) == 'string' and fmt:match('%S') then if fmt:find('%', 1, true) then return strftime(date, fmt, options) end elseif date.partial then fmt = date.month and 'my' or 'y' else fmt = 'dmy' if date.hastime then fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt end end local function bad_format() -- For consistency with other format processing, return given format -- (or cleaned format if original was not a string) if invalid. return mw.text.nowiki(fmt) end if date.partial then -- Ignore days in standard formats like 'ymd'. if fmt == 'ym' or fmt == 'ymd' then fmt = date.month and '%Y-%m %{era}' or '%Y %{era}' elseif fmt == 'my' or fmt == 'dmy' or fmt == 'mdy' then fmt = date.month and '%B %-Y %{era}' or '%-Y %{era}' elseif fmt == 'y' then fmt = date.month and '%-Y %{era}' or '%-Y %{era}' else return bad_format() end return strftime(date, fmt, options) end local function hm_fmt() local plain = make_option_table(options, date.options).bydefault.am return plain and '%H:%M' or '%-I:%M %p' end local need_time = date.hastime local t = collection() for item in fmt:gmatch('%S+') do local f if item == 'hm' then f = hm_fmt() need_time = false elseif item == 'hms' then f = '%H:%M:%S' need_time = false elseif item == 'ymd' then f = '%Y-%m-%d %{era}' elseif item == 'mdy' then f = '%B %-d, %-Y %{era}' elseif item == 'dmy' then f = '%-d de %B de %-Y %{era}' else return bad_format() end t:add(f) end fmt = t:join(' ') if need_time then fmt = hm_fmt() .. ' ' .. fmt end return strftime(date, fmt, options) end local day_info = { -- 0=Sun to 6=Sat [0] = { 'do', 'domingo' }, { 'lu', 'lunes' }, { 'ma', 'martes' }, { 'mi', 'miércoles' }, { 'ju', 'jueves' }, { 'vi', 'viernes' }, { 'sa', 'sábado' }, } local month_info = { -- 1=Jan to 12=Dec { 'ene', 'enero' }, { 'feb', 'febrero' }, { 'mar', 'marzo' }, { 'abr', 'abril' }, { 'may', 'mayo' }, { 'jun', 'junio' }, { 'jul', 'julio' }, { 'ago', 'agosto' }, { 'sep', 'septiembre' }, { 'oct', 'octubre' }, { 'nov', 'noviembre' }, { 'dic', 'diciembre' }, } for k,v in pairs(month_info) do month_info[ v[1] ], month_info[ v[2] ] = v, v end local function name_to_number(text, translate) if type(text) == 'string' then return translate['xx' .. text:lower():gsub('é', 'e'):gsub('á', 'a')] end end local function day_number(text) return name_to_number(text, { xxdo = 0, xxdomingo = 0, xxlu = 1, xxlunes = 1, xxlune = 1, xxma = 2, xxmartes = 2, xxmarte = 2, xxmi = 3, xxmiercoles = 3, xxmiercole = 3, xxju = 4, xxjueves = 4, xxjueve = 4, xxvi = 5, xxviernes = 5, xxvierne = 5, xxsat = 6, xxsabado = 6 }) end local function month_number(text) return name_to_number(text, { xxene = 1, xxenero = 1, xxfeb = 2, xxfebrero = 2, xxmar = 3, xxmarzo = 3, xxabr = 4, xxabril = 4, xxmay = 5, xxmayo = 5, xxjun = 6, xxjunio = 6, xxjul = 7, xxjulio = 7, xxago = 8, xxagosto = 8, xxsep = 9, xxseptiembre = 9, xxsept = 9, xxoct = 10, xxoctubre = 10, xxnov = 11, xxnoviembre = 11, xxdic = 12, xxdiciembre = 12, }) end local function _list_text(list, fmt) -- Return a list of formatted strings from a list of dates. if not type(list) == 'table' then error('date:list:text: need "list:text()" with a colon', 2) end local result = { join = _list_join } for i, date in ipairs(list) do result[i] = date:text(fmt) end return result end local function _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 >' if not is_date(date) then error('date:list: need a date (use "date:list()" with a colon)', 2) end local list = { text = _list_text } if date.partial then return list end local count, offset, operation local ops = { ['>='] = { before = false, include = true }, ['>'] = { before = false, include = false }, ['<='] = { before = true , include = true }, ['<'] = { before = true , include = false }, } if spec then if type(spec) == 'number' then count = floor(spec + 0.5) if count < 0 then count = -count operation = ops['<'] end elseif type(spec) == 'string' then local num, day, op = spec:match('^%s*(%d*)%s*(%a*)%s*([<>=]*)%s*$') if not num then return list end if num ~= '' then count = tonumber(num) end if day ~= '' then local dow = day_number(day:gsub('[sS]$', '')) -- accept plural days if not dow then return list end offset = dow - date.dow end operation = ops[op] else return list end end offset = offset or 0 operation = operation or ops['>'] local datefrom, dayfirst, daylast if operation.before then if offset > 0 or (offset == 0 and not operation.include) then offset = offset - 7 end if count then if count > 1 then offset = offset - 7*(count - 1) end datefrom = date + offset else daylast = date.day + offset dayfirst = daylast % 7 if dayfirst == 0 then dayfirst = 7 end end else if offset < 0 or (offset == 0 and not operation.include) then offset = offset + 7 end if count then datefrom = date + offset else dayfirst = date.day + offset daylast = date.monthdays end end if not count then if daylast < dayfirst then return list end count = floor((daylast - dayfirst)/7) + 1 datefrom = Date(date, {day = dayfirst}) end for i = 1, count do if not datefrom then break end -- exceeds date limits list[i] = datefrom datefrom = datefrom + 7 end return list end -- A table to get the current date/time (UTC), but only if needed. local current = setmetatable({}, { __index = function (self, key) local d = 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 return rawget(self, key) end }) local function extract_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 [partially] rejected as potentially ambiguous. local date, options = {}, {} if text: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'. local sign, y, m, d, H, M, S = text:match('^([+%-])(%d+)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)Z$') if sign then y = tonumber(y) if sign == '-' and y > 0 then y = -y end if y <= 0 then options.era = 'a. C.' -- Sets the era when the year is negative on the timestamp end date.year = y m = tonumber(m) d = tonumber(d) H = tonumber(H) M = tonumber(M) S = tonumber(S) if m == 0 then newdate.partial = true return date, options end date.month = m if d == 0 then newdate.partial = true return date, options end date.day = d if H > 0 or M > 0 or S > 0 then date.hour = H date.minute = M date.second = S end return date, options end return end local a, b, c = text:match('^(%d+)[-./](%d+)[-./](%d+)$') if a --[[ and b and c --]] then a = tonumber(a) b = tonumber(b) c = tonumber(c) --[[ -- use extract_ymd for this if a > 31 and m <= 12 and c > 12 then date.year, date.month, date.day = a, b, c options.format = 'ymd' newdate.partial = true return date, options else--]] if a > 12 and b <= 12 and c > 31 then date.year, date.month, date.day = c, b, a options.format = 'dmy' newdate.partial = true return date, options elseif a <= 12 and b > 12 and c > 31 then date.year, date.month, date.day = c, a, b options.format = 'mdy' newdate.partial = true return date, options end end local function extract_ymd(item) -- Called when no day or month has been set. local y, m, d = item:match('^(%d%d%d%d)[-./](%w+)[-./](%d%d?)$') if y then if date.year then return end if m:match('^%d%d?$') then m = tonumber(m) else m = month_number(m) end if m then date.year = tonumber(y) date.month = m date.day = tonumber(d) return true end end end local function extract_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. local number, suffix = item:match('^(%d%d?%d?%d?)(.*)$') if number then local n = tonumber(number) if #number <= 2 and n <= 31 then suffix = suffix:lower() if suffix == '' or suffix == 'st' or suffix == 'nd' or suffix == 'rd' or suffix == 'th' then date.day = n return true end elseif suffix == '' and newdate.partial and not date.year then date.year = n return true end end end local function extract_month(item) -- A month must be given as a name or abbreviation; a number could be ambiguous. local m = month_number(item) if m then date.month = m return true end end local function extract_time(item) local h, m, s = item:match('^(%d%d?):(%d%d)(:?%d*)$') if date.hour or not h then return end if s ~= '' then s = s:match('^:(%d%d)$') if not s then return end end date.hour = tonumber(h) date.minute = tonumber(m) date.second = tonumber(s) -- nil if empty string return true end local item_count = 0 local index_time local function set_ampm(item) local H = date.hour if H and not options.am and index_time + 1 == item_count then options.am = ampm_options[item] -- caller checked this is not nil if item:match('^[Aa]') then if not (1 <= H and H <= 12) then return end if H == 12 then date.hour = 0 end else if not (1 <= H and H <= 23) then return end if H <= 11 then date.hour = H + 12 end end return true end end --Filtrar abreviaturas de era for key,value in pairs(era_text) do if string.find(text, key) ~= nil then options.era = key text = string.gsub(text, key, '') break end end for item in text:gsub(',', ' '):gsub('del?', ''):gsub(' ', ' '):gmatch('%S+') do item_count = item_count + 1 if era_text[item] then -- Era is accepted in peculiar places. if options.era then return end options.era = item elseif ampm_options[item] then if not set_ampm(item) then return end elseif item:find(':', 1, true) then if not extract_time(item) then return end index_time = item_count elseif date.day and date.month then if date.year then return -- should be nothing more so item is invalid end if not item:match('^(%d%d?%d?%d?)$') then return end date.year = tonumber(item) elseif day_number(item) then --catch month day case elseif date.day then if not extract_month(item) then return end elseif date.month then if not extract_day_or_year(item) then return end elseif extract_month(item) then options.format = 'mdy' elseif extract_ymd(item) then options.format = 'ymd' elseif extract_day_or_year(item) then if date.day then options.format = 'dmy' end else --return not return if item not recognized end end if not date.year or date.year == 0 then return end local era = era_text[options.era] if era and era.isbc then date.year = 1 - date.year end return date, options end local function autofill(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). local function filled(a, b) local fillmonth, fillday if not a.month then fillmonth = b.month or 1 end if not a.day then fillday = b.day or 1 end if fillmonth or fillday then -- need to create a new date if (fillmonth or a.month) == 2 and (fillday or a.day) == 29 then -- Avoid invalid date, for example with {{age|2013|29 Feb 2016}} or {{age|Feb 2013|29 Jan 2015}}. if not is_leap_year(a.year, a.calendar) then fillday = 28 end end a = Date(a, { month = fillmonth, day = fillday }) end return a end return filled(date1, date2), filled(date2, date1) end local function date_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. if lhs.partial then -- 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 local function is_prefix(text, word, minlen) local n = #text return (minlen or 1) <= n and n <= #word and text == word:sub(1, n) end local function do_days(n) local forcetime, jd if floor(n) == n then jd = lhs.jd else forcetime = not lhs.hastime jd = lhs.jdz end jd = jd + (is_sub and -n or n) if forcetime then jd = tostring(jd) if not jd:find('.', 1, true) then jd = jd .. '.0' end end return Date(lhs, 'juliandate', jd) end if type(rhs) == 'number' then -- Add/subtract days, including fractional days. return do_days(rhs) end if type(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. local sign, numstr, id = rhs:match('^%s*([+-]?)([%d%.]+)%s*(%a+)$') if sign then if sign == '-' then is_sub = not (is_sub and true or false) end local y, m, days local num = tonumber(numstr) if not num then return end id = id:lower() if is_prefix(id, 'years') then y = num m = 0 elseif is_prefix(id, 'months') then y = floor(num / 12) m = num % 12 elseif is_prefix(id, 'weeks') then days = num * 7 elseif is_prefix(id, 'days') then days = num elseif is_prefix(id, 'hours') then days = num / 24 elseif is_prefix(id, 'minutes', 3) then days = num / (24 * 60) elseif is_prefix(id, 'seconds') then days = num / (24 * 3600) else return end if days then return do_days(days) end if numstr:find('.', 1, true) then return end if is_sub then y = -y m = -m end assert(-11 <= m and m <= 11) y = lhs.year + y m = lhs.month + m if m > 12 then y = y + 1 m = m - 12 elseif m < 1 then y = y - 1 m = m + 12 end local d = math.min(lhs.day, days_in_month(y, m, lhs.calendar)) return Date(lhs, y, m, d) end end if is_diff(rhs) then local days = rhs.age_days if (is_sub or false) ~= (rhs.isnegative or false) then days = -days end return lhs + days end end local full_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. local datemt = { __index = function (self, key) if rawget(self, 'partial') then if full_date_only[key] then return end if key == 'monthabbr' or key == 'monthdays' or key == 'monthname' then if not self.month then return end end end local value if key == 'dayabbr' then value = day_info[self.dow][1] elseif key == 'dayname' then value = day_info[self.dow][2] elseif key == 'dow' then value = (self.jdnoon + 1) % 7 -- day-of-week 0=Sun to 6=Sat elseif key == 'dayofweek' then value = self.dow elseif key == 'dowiso' then value = (self.jdnoon % 7) + 1 -- ISO day-of-week 1=Mon to 7=Sun elseif key == 'dayofweekiso' then value = self.dowiso elseif key == 'dayofyear' then local first = Date(self.year, 1, 1, self.calendar).jdnoon value = self.jdnoon - first + 1 -- day-of-year 1 to 366 elseif key == 'era' then -- Era text (never a negative sign) from year and options. value = get_era_for_year(self.options.era, self.year) elseif key == 'format' then value = self.options.format or 'dmy' elseif key == '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) elseif key == 'juliandate' or key == 'jd' or key == 'jdz' then local jd, jdz = julian_date(self) rawset(self, 'juliandate', jd) rawset(self, 'jd', jd) rawset(self, 'jdz', jdz) return key == 'jdz' and jdz or jd elseif key == 'jdnoon' then -- Julian date at noon (an integer) on the calendar day when jd occurs. value = floor(self.jd + 0.5) elseif key == 'isleapyear' then value = is_leap_year(self.year, self.calendar) elseif key == 'monthabbr' then value = month_info[self.month][1] elseif key == 'monthdays' then value = days_in_month(self.year, self.month, self.calendar) elseif key == 'monthname' then value = month_info[self.month][2] end if value ~= nil then rawset(self, key, value) return value end end, } -- Date operators. local function mt_date_add(lhs, rhs) if not is_date(lhs) then lhs, rhs = rhs, lhs -- put date on left (it must be a date for this to have been called) end return date_add_sub(lhs, rhs) end local function mt_date_sub(lhs, rhs) if is_date(lhs) then if is_date(rhs) then return DateDiff(lhs, rhs) end return date_add_sub(lhs, rhs, true) end end local function mt_date_concat(lhs, rhs) return tostring(lhs) .. tostring(rhs) end local function mt_date_tostring(self) return self:text() end local function mt_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. if lhs.partial or rhs.partial then -- One date is partial; the other is a partial or a full date. -- The months may both be nil, but must be the same. return lhs.year == rhs.year and lhs.month == rhs.month and lhs.calendar == rhs.calendar end return lhs.jdz == rhs.jdz end local function mt_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. if lhs.partial or rhs.partial then -- One date is partial; the other is a partial or a full date. if lhs.calendar ~= rhs.calendar then return lhs.calendar == 'Julian' end if lhs.partial then lhs = lhs.partial.first end if rhs.partial then rhs = rhs.partial.first end end return lhs.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 ]] function Date(...) -- 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). local calendars = { julian = 'Julian', gregorian = 'Gregorian' } local newdate = { _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) return DateDiff(self, rhs, options) end, text = _date_text, } local argtype, datetext, is_copy, jd_number, tnums local numindex = 0 local numfields = { 'year', 'month', 'day', 'hour', 'minute', 'second' } local numbers = {} for _, v in ipairs({...}) do v = strip_to_nil(v) local vlower = type(v) == 'string' and v:lower() or nil if v == nil then -- Ignore empty arguments after stripping so modules can directly pass template parameters. elseif calendars[vlower] then newdate.calendar = calendars[vlower] elseif vlower == 'partial' then newdate.partial = true elseif vlower == 'fix' then newdate.want_fix = true elseif is_date(v) then -- Copy existing date (items can be overridden by other arguments). if is_copy or tnums then 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 elseif type(v) == 'table' then if tnums then return end tnums = {} local tfields = { year=1, month=1, day=1, hour=2, minute=2, second=2 } for tk, tv in pairs(v) do if tfields[tk] then tnums[tk] = tonumber(tv) end if tfields[tk] == 2 then newdate.hastime = true end end else local num = tonumber(v) if not num and argtype == 'setdate' and numindex == 1 then num = month_number(v) end if num then if not argtype then argtype = 'setdate' end if argtype == 'setdate' and numindex < 6 then numindex = numindex + 1 numbers[numfields[numindex]] = num elseif argtype == 'juliandate' and not jd_number then jd_number = num if type(v) == 'string' then if v:find('.', 1, true) then newdate.hastime = true end elseif num ~= 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 elseif argtype then return elseif type(v) == 'string' then if v == 'currentdate' or v == 'currentdatetime' or v == 'juliandate' then argtype = v else argtype = 'datetext' datetext = v end else return end end end if argtype == 'datetext' then if tnums or not set_date_from_numbers(newdate, extract_date(newdate, datetext)) then return end elseif argtype == 'juliandate' then newdate.partial = nil newdate.jd = jd_number if not set_date_from_jd(newdate) then return end elseif argtype == 'currentdate' or argtype == 'currentdatetime' then newdate.partial = nil newdate.year = current.year newdate.month = current.month newdate.day = current.day if argtype == '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 elseif argtype == 'setdate' then if tnums or not set_date_from_numbers(newdate, numbers) then return end elseif not (is_copy or tnums) then return end if tnums then newdate.jd = nil -- force recalculation in case jd was set before changes from tnums if not set_date_from_numbers(newdate, tnums) then return end end if newdate.partial then local year = newdate.year local month = newdate.month local first = Date(year, month or 1, 1, newdate.calendar) month = month or 12 local last = 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) local readonly = {} local mt = { __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, } return setmetatable(readonly, mt) end local function _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. local wantround, wantduration, wantrange if type(options) == 'table' then wantround = options.round wantduration = options.duration wantrange = options.range else wantround = options end if not is_diff(diff) then local f = wantduration and 'duration' or 'age' error(f .. ': need a date difference (use "diff:' .. f .. '()" with a colon)', 2) end if diff.partial then -- Ignore wantround, wantduration. local function choose(v) if type(v) == 'table' then if not wantrange or v[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. return v[2] end end return v end if code == 'ym' or code == 'ymd' then if not wantrange and diff.iszero then -- 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. return 0, 0 end return choose(diff.partial.years), choose(diff.partial.months) end if code == 'y' then return choose(diff.partial.years) end if code == 'm' or code == 'w' or code == 'd' then return choose({ diff.partial.mindiff:age(code), diff.partial.maxdiff:age(code) }) end return nil end local extra_days = wantduration and 1 or 0 if code == 'wd' or code == 'w' or code == 'd' then local offset = wantround and 0.5 or 0 local days = diff.age_days + extra_days if code == 'wd' or code == 'd' then days = floor(days + offset) if code == 'd' then return days end return floor(days/7), days % 7 end return floor(days/7 + offset) end local H, M, S = diff.hours, diff.minutes, diff.seconds if code == 'dh' or code == 'dhm' or code == 'dhms' or code == 'h' or code == 'hm' or code == 'hms' then local days = floor(diff.age_days + extra_days) local inc_hour if wantround then if code == 'dh' or code == 'h' then if M >= 30 then inc_hour = true end elseif code == 'dhm' or code == 'hm' then if S >= 30 then M = M + 1 if M >= 60 then M = 0 inc_hour = true end end else -- Nothing needed because S is an integer. end if inc_hour then H = H + 1 if H >= 24 then H = 0 days = days + 1 end end end if code == 'dh' or code == 'dhm' or code == 'dhms' then if code == 'dh' then return days, H elseif code == 'dhm' then return days, H, M else return days, H, M, S end end local hours = days * 24 + H if code == 'h' then return hours elseif code == 'hm' then return hours, M end return hours, M, S end if wantround then local inc_hour if code == 'ymdh' or code == 'ymwdh' then if M >= 30 then inc_hour = true end elseif code == 'ymdhm' or code == 'ymwdhm' then if S >= 30 then M = M + 1 if M >= 60 then M = 0 inc_hour = true end end elseif code == 'ymd' or code == 'ymwd' or code == 'yd' or code == 'md' then if H >= 12 then extra_days = extra_days + 1 end end if inc_hour then H = H + 1 if H >= 24 then H = 0 extra_days = extra_days + 1 end end end local y, m, d = diff.years, diff.months, diff.days if extra_days > 0 then d = d + extra_days if d > 28 or code == '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 if code == 'ymd' then return y, m, d elseif code == 'yd' then if y > 0 then -- It is known that diff.date1 > diff.date2. diff = diff.date1 - (diff.date2 + (y .. 'y')) end return y, floor(diff.age_days) elseif code == 'md' then return y * 12 + m, d elseif code == 'ym' or code == 'm' then if wantround then if d >= 16 then m = m + 1 if m >= 12 then m = 0 y = y + 1 end end end if code == 'ym' then return y, m end return y * 12 + m elseif code == 'ymw' then local weeks = floor(d/7) if wantround then local days = d % 7 if days > 3 or (days == 3 and H >= 12) then weeks = weeks + 1 end end return y, m, weeks elseif code == 'ymwd' then return y, m, floor(d/7), d % 7 elseif code == 'ymdh' then return y, m, d, H elseif code == 'ymwdh' then return y, m, floor(d/7), d % 7, H elseif code == 'ymdhm' then return y, m, d, H, M elseif code == 'ymwdhm' then return y, m, floor(d/7), d % 7, H, M end if code == 'y' then if wantround and m >= 6 then y = y + 1 end return y end return nil end local function _diff_duration(diff, code, options) if type(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) return tostring(lhs) .. tostring(rhs) end, __tostring = function (self) return tostring(self.age_days) end, __index = function (self, key) local value if key == 'age_days' then if rawget(self, 'partial') then local function jdz(date) return (date.partial and date.partial.first or date).jdz end value = jdz(self.date1) - jdz(self.date2) else value = self.date1.jdz - self.date2.jdz end end if value ~= nil then rawset(self, key, value) return value end end, } function DateDiff(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). if not (is_date(date1) and is_date(date2) and date1.calendar == date2.calendar) then return end local wantfill if type(options) == 'table' then wantfill = options.fill end local isnegative = false local iszero = false if date1 < date2 then isnegative = true date1, date2 = date2, date1 elseif date1 == date2 then iszero = true end -- It is known that date1 >= date2 (period is from date2 to date1). if date1.partial or date2.partial then -- 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 if wantfill then date1, date2 = autofill(date1, date2) else local function zdiff(date1, date2) local diff = date1 - date2 if diff.isnegative then return date1 - date1 -- a valid diff in case we call its methods end return diff end local function getdate(date, which) return date.partial and date.partial[which] or date end local maxdiff = zdiff(getdate(date1, 'last'), getdate(date2, 'first')) local mindiff = zdiff(getdate(date1, 'first'), getdate(date2, 'last')) local years, months if maxdiff.years == mindiff.years then years = maxdiff.years if maxdiff.months == mindiff.months then months = maxdiff.months else months = { mindiff.months, maxdiff.months } end else years = { mindiff.years, maxdiff.years } end return setmetatable({ 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 local y1, m1 = date1.year, date1.month local y2, m2 = date2.year, date2.month local years = y1 - y2 local months = m1 - m2 local d1 = date1.day + hms(date1) local d2 = date2.day + hms(date2) local days, time if d1 >= d2 then days = d1 - d2 else months = months - 1 -- Get days in previous month (before the "to" date) given December has 31 days. local dpm = m1 > 1 and days_in_month(y1, m1 - 1, date1.calendar) or 31 if d2 >= dpm then days = d1 - hms(date2) else days = dpm - d2 + d1 end end if months < 0 then years = years - 1 months = months + 12 end days, time = math.modf(days) local H, M, S = h_m_s(time) return setmetatable({ 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 local z = { _current = current, _Date = Date, _days_in_month = days_in_month, } -- Aqui comienzas las funciones adaptadas de [[Módulo:Fecha]] -- ---------------------------------------------------------------- ---------------------------------------------------------------- ---------------------------------------------------------------- function z.fechaActual() local d = os.date('!*t') local fecha = {} fecha.anyo = d.year fecha.mes = d.month fecha.dia = d.day fecha.hora = d.hour fecha.minuto = d.min fecha.segundo = d.sec return fecha end function validar(fecha) fecha.anyo = tonumber(fecha.anyo) fecha.mes = tonumber(fecha.mes) fecha.dia = tonumber(fecha.dia) fecha.hora = tonumber(fecha.hora) fecha.minuto = tonumber(fecha.minuto) fecha.segundo = tonumber(fecha.segundo) -- Falta validar que es una fecha válida end function z.edad(fecha1, fecha2) --Función que devuelve la edad en años entre dos fechas --Se supone que las fechas se han validado previamente. if not fecha1 then return -- falta devolver un error end if not fecha2 then fecha2=z.fechaActual() end local anyos = fecha2.anyo - fecha1.anyo --if true then return require('Módulo:Tablas').tostring(fecha2) end if fecha2.mes < fecha1.mes or (fecha2.mes == fecha1.mes and fecha2.dia < fecha1.dia) then anyos = anyos - 1 end if anyos < 0 then return -- falta devolver error elseif anyos == 0 then return 'menos de un año' elseif anyos == 1 then return 'un año' else return anyos .. ' años' end end function z.llamadaDesdeUnaPlantilla(frame) function obtenerFecha(dia, mes, anyo) local resultado={} if dia then resultado.dia = dia resultado.mes = mes resultado.anyo = anyo validar(resultado) return resultado end end local args = frame.args local funcion = z[args[1]] local fecha1 = obtenerFecha(args[2], args[3], args[4]) local fecha2 = obtenerFecha(args[5], args[6], args[7]) return funcion(fecha1, fecha2) end -- Aqui comienzas las funciones adaptadas de [[Módulo:Fechas]] -- ----------------------------------------------------------------- ----------------------------------------------------------------- ----------------------------------------------------------------- function z.NombreDelMes(mes) -- Función que devuelve el nombre del mes, donde mes es un número entre 1 y 12. -- Si no es así se devuelve el valor de mes. -- Por ejemplo, 2 --> febrero -- 02 --> febrero -- abril --> abril -- MAYO --> MAYO return month_info[tonumber(mes)][2] or mes end function z.Fecha(frame) -- Función que formatea una fecha -- El único parámetro obligatorio es el año o 3. -- Obtener los argumentos con los que se llama a la función local argumentos = {} local parent = {} if frame == mw.getCurrentFrame() then if frame.args[3] or frame.args["año"] then argumentos = frame.args else parent = frame:getParent() argumentos = parent.args end else argumentos = frame end local enlace = argumentos["enlace"] ~= "no" -- Obtener el día, el nombre del mes y el año incluyendo para los años negativos a.d. local dia = argumentos["día"] or argumentos[1] or '' local mes = argumentos["mes"] or argumentos[2] or '' local anyo=tonumber(argumentos["año"] or argumentos[3]) or 0 dia = (dia ~='') and (tonumber(dia) or dia) or dia -- Eliminar ceros a la izquierda del día. mes = (mes~='') and ((month_info[mes] and month_info[mes][2]) or month_info[tonumber(mes)][2] or mes) or mes -- Extraer nombre del mes anyo = (anyo<0) and (-anyo .. ' a. C.') or anyo local calendario = (argumentos["calendario"] == 'juliano') and ('<sup>[[Calendario juliano|jul.]]</sup>') or '' -- Formatear la fecha dependiendo de si el día, el mes o el año están informados local out = '' if anyo ~= 0 then out = enlace and (out .. '[[' .. anyo .. ']]') or tostring(anyo) if mes~='' then if argumentos["mayúscula"] == 'sí' then mes = mw.language.new('es'):ucfirst(mes) end out = enlace and (mes .. ']] de ' .. out) or (mes .. ' de ' .. out) if dia ~='' then out = enlace and ('[[' .. dia .. ' de ' .. out .. calendario) or (dia .. ' de ' .. out .. calendario) else out = enlace and ('[[' .. out) or out end end end return out end function z.Numerica(frame) local d = Date(frame.args[1]) local err = '<strong class="error">Cadena de fecha no válida</strong>' return (d == nil) and err or d:text('%Y%m%d') end return z