置顶公告:【置顶】关于临时开启评论区所有功能的公告(2022.10.22) | 【置顶】关于本站Widget恢复使用的公告
  • 你好~!欢迎来到萌娘百科镜像站!如需查看或编辑,请联系本站管理员注册账号。
  • 本镜像站和其他萌娘百科的镜像站无关,请注意分别。

Module:Citation/CS1/Date validation

贴贴♀百科,万娘皆可贴的百科全书!转载请标注来源页面的网页链接,并声明引自贴贴百科。内容不可商用。
跳到导航 跳到搜索
Template-info.svg 模块文档  [查看] [编辑] [历史] [刷新]

这个模块及其相关的下级模块支持Citation Style 1以及Citation Style 2格式的引文模板。这两者通常简称为CS1和CS2。

cs1 • cs2 模块
在用的 沙盒 简介
Module:Citation/CS1 Module:Citation/CS1/sandbox [编辑] Rendering and support functions
Module:Citation/CS1/Configuration Module:Citation/CS1/Configuration/sandbox [编辑] Translation tables; error and identifier handlers
Module:Citation/CS1/Whitelist Module:Citation/CS1/Whitelist/sandbox [编辑] List of active, deprecated, and obsolete cs1 • 2 parameters
Module:Citation/CS1/Date validation Module:Citation/CS1/Date validation/sandbox [编辑] Date format validation functions
Module:Citation/CS1/Identifiers Module:Citation/CS1/Identifiers/sandbox [编辑] Functions that support the named identifiers (isbn, doi, pmid, etc)
Module:Citation/CS1/Utilities Module:Citation/CS1/Utilities/sandbox [编辑] Common functions and tables
Module:Citation/CS1/COinS Module:Citation/CS1/COinS/sandbox [编辑] Functions that render a cs1 • 2 template's metadata
Module:Citation/CS1/Suggestions Module:Citation/CS1/Suggestions/sandbox [编辑] List that maps common erroneous parameter names to valid parameter names
  1. local p = {}
  2. --[[--------------------------< I S _ V A L I D _ A C C E S S D A T E >----------------------------------------
  3. returns true if:
  4. Wikipedia start date <= accessdate < today + 2 days
  5. Wikipedia start date is 2001-01-15T00:00:00 UTC which is 979516800 seconds after 1970-01-01T00:00:00 UTC (the start of Unix time)
  6. accessdate is the date provided in |accessdate= at time 00:00:00 UTC
  7. today is the current date at time 00:00:00 UTC plus 48 hours
  8. if today is 2015-01-01T00:00:00 then
  9. adding 24 hours gives 2015-01-02T00:00:00 – one second more than today
  10. adding 24 hours gives 2015-01-03T00:00:00 – one second more than tomorrow
  11. ]]
  12. local function is_valid_accessdate (accessdate)
  13. local lang = mw.getContentLanguage();
  14. local good1, good2;
  15. local access_ts, tomorrow_ts; -- to hold unix time stamps representing the dates
  16. accessdate = accessdate:gsub("年", "-");
  17. accessdate = accessdate:gsub("月", "-");
  18. accessdate = accessdate:gsub("日", "-");
  19. accessdate = accessdate:gsub("-$", "");
  20. good1, access_ts = pcall( lang.formatDate, lang, 'U', accessdate ); -- convert accessdate value to unix timesatmp
  21. good2, tomorrow_ts = pcall( lang.formatDate, lang, 'U', 'today + 2 days' ); -- today midnight + 2 days is one second more than all day tomorrow
  22. if good1 and good2 then
  23. access_ts = tonumber (access_ts); -- convert to numbers for the comparison
  24. tomorrow_ts = tonumber (tomorrow_ts);
  25. else
  26. return false; -- one or both failed to convert to unix time stamp
  27. end
  28. if 979516800 <= access_ts and access_ts < tomorrow_ts then -- Wikipedia start date <= accessdate < tomorrow's date
  29. return true;
  30. else
  31. return false; -- accessdate out of range
  32. end
  33. end
  34. --[[--------------------------< G E T _ M O N T H _ N U M B E R >----------------------------------------------
  35. returns a number according to the month in a date: 1 for January, etc. Capitalization and spelling must be correct. If not a valid month, returns 0
  36. ]]
  37. local function get_month_number (month)
  38. local long_months = {['January']=1, ['February']=2, ['March']=3, ['April']=4, ['May']=5, ['June']=6, ['July']=7, ['August']=8, ['September']=9, ['October']=10, ['November']=11, ['December']=12};
  39. local short_months = {['Jan']=1, ['Feb']=2, ['Mar']=3, ['Apr']=4, ['May']=5, ['Jun']=6, ['Jul']=7, ['Aug']=8, ['Sep']=9, ['Oct']=10, ['Nov']=11, ['Dec']=12};
  40. local zh_months = {['1月']=1, ['2月']=2, ['3月']=3, ['4月']=4, ['5月']=5, ['6月']=6, ['7月']=7, ['8月']=8, ['9月']=9, ['10月']=10, ['11月']=11, ['12月']=12}; -- LOCAL
  41. local temp;
  42. temp=long_months[month];
  43. if temp then return temp; end -- if month is the long-form name
  44. temp=short_months[month];
  45. if temp then return temp; end -- if month is the short-form name
  46. temp=zh_months[month]; -- LOCAL
  47. if temp then return temp; end -- if month is in Chinese -- LOCAL
  48. return 0; -- misspelled, improper case, or not a month name
  49. end
  50. --[[--------------------------< G E T _ S E A S O N _ N U M B E R >--------------------------------------------
  51. returns a number according to the sequence of seasons in a year: 1 for Winter, etc. Capitalization and spelling must be correct. If not a valid season, returns 0
  52. ]]
  53. local function get_season_number (season)
  54. local season_list = {['Winter']=21, ['Spring']=22, ['Summer']=23, ['Fall']=24, ['Autumn']=24}; -- make sure these numbers do not overlap month numbers
  55. local temp;
  56. temp=season_list[season];
  57. if temp then return temp; end -- if season is a valid name return its number
  58. return 0; -- misspelled, improper case, or not a season name
  59. end
  60. --[[--------------------------< I S _ P R O P E R _ N A M E >--------------------------------------------------
  61. returns a non-zero number if date contains a recognized proper name. Capitalization and spelling must be correct.
  62. ]]
  63. local function is_proper_name (name)
  64. local name_list = {['Christmas']=31}
  65. local temp;
  66. temp=name_list[name];
  67. if temp then return temp; end -- if name is a valid name return its number
  68. return 0; -- misspelled, improper case, or not a proper name
  69. end
  70. --[[--------------------------< I S _ V A L I D _ M O N T H _ O R _ S E A S O N >------------------------------
  71. --returns true if month or season is valid (properly spelled, capitalized, abbreviated)
  72. ]]
  73. local function is_valid_month_or_season (month_season)
  74. if 0 == get_month_number (month_season) then -- if month text isn't one of the twelve months, might be a season
  75. if 0 == get_season_number (month_season) then -- not a month, is it a season?
  76. return false; -- return false not a month or one of the five seasons
  77. end
  78. end
  79. return true;
  80. end
  81. --[[--------------------------< I S _ V A L I D _ Y E A R >----------------------------------------------------
  82. Function gets current year from the server and compares it to year from a citation parameter. Years more than one year in the future are not acceptable.
  83. ]]
  84. local function is_valid_year(year)
  85. if not is_set(year_limit) then
  86. year_limit = tonumber(os.date("%Y"))+1; -- global variable so we only have to fetch it once
  87. end
  88. return tonumber(year) <= year_limit; -- false if year is in the future more than one year
  89. end
  90. --[[--------------------------< I S _ V A L I D _ D A T E >----------------------------------------------------
  91. Returns true if day is less than or equal to the number of days in month and year is no farther into the future
  92. than next year; else returns false.
  93. Assumes Julian calendar prior to year 1582 and Gregorian calendar thereafter. Accounts for Julian calendar leap
  94. years before 1582 and Gregorian leap years after 1582. Where the two calendars overlap (1582 to approximately
  95. 1923) dates are assumed to be Gregorian.
  96. ]]
  97. local function is_valid_date (year, month, day)
  98. local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  99. local month_length;
  100. if not is_valid_year(year) then -- no farther into the future than next year
  101. return false;
  102. end
  103. month = tonumber(month); -- required for YYYY-MM-DD dates
  104. if (2==month) then -- if February
  105. month_length = 28; -- then 28 days unless
  106. if 1582 > tonumber(year) then -- Julian calendar
  107. if 0==(year%4) then
  108. month_length = 29;
  109. end
  110. else -- Gregorian calendar
  111. if (0==(year%4) and (0~=(year%100) or 0==(year%400))) then -- is a leap year?
  112. month_length = 29; -- if leap year then 29 days in February
  113. end
  114. end
  115. else
  116. month_length=days_in_month[month];
  117. end
  118. if tonumber (day) > month_length then
  119. return false;
  120. end
  121. return true;
  122. end
  123. --[[--------------------------< I S _ V A L I D _ M O N T H _ R A N G E _ S T Y L E >--------------------------
  124. Months in a range are expected to have the same style: Jan–Mar or October–December but not February–Mar or Jul–August.
  125. There is a special test for May because it can be either short or long form.
  126. Returns true when style for both months is the same
  127. ]]
  128. local function is_valid_month_range_style (month1, month2)
  129. local len1 = month1:len();
  130. local len2 = month2:len();
  131. if len1 == len2 then
  132. return true; -- both months are short form so return true
  133. elseif 'May' == month1 or 'May'== month2 then
  134. return true; -- both months are long form so return true
  135. elseif 3 == len1 or 3 == len2 then
  136. return false; -- months are mixed form so return false
  137. else
  138. return true; -- both months are long form so return true
  139. end
  140. end
  141. --[[--------------------------< I S _ V A L I D _ M O N T H _ S E A S O N _ R A N G E >------------------------
  142. Check a pair of months or seasons to see if both are valid members of a month or season pair.
  143. Month pairs are expected to be left to right, earliest to latest in time.
  144. Similarly, seasons are also left to right, earliest to latest in time. There is an oddity with seasons: winter is assigned a value of 1, spring 2, ...,
  145. fall and autumn 4. Because winter can follow fall/autumn at the end of a calender year, a special test is made to see if |date=Fall-Winter yyyy (4-1) is the date.
  146. ]]
  147. local function is_valid_month_season_range(range_start, range_end)
  148. local range_start_number = get_month_number (range_start);
  149. if 0 == range_start_number then -- is this a month range?
  150. local range_start_number = get_season_number (range_start); -- not a month; is it a season? get start season number
  151. local range_end_number = get_season_number (range_end); -- get end season number
  152. if 0 ~= range_start_number then -- is start of range a season?
  153. if range_start_number < range_end_number then -- range_start is a season
  154. return true; -- return true when range_end is also a season and follows start season; else false
  155. end
  156. if 24 == range_start_number and 21 == range_end_number then -- special case when season range is Fall-Winter or Autumn-Winter
  157. return true;
  158. end
  159. end
  160. return false; -- range_start is not a month or a season; or range_start is a season and range_end is not; or improper season sequence
  161. end
  162. local range_end_number = get_month_number (range_end); -- get end month number
  163. if range_start_number < range_end_number then -- range_start is a month; does range_start precede range_end?
  164. if is_valid_month_range_style (range_start, range_end) then -- do months have the same style?
  165. return true; -- proper order and same style
  166. end
  167. end
  168. return false; -- range_start month number is greater than or equal to range end number; or range end isn't a month
  169. end
  170. --[[--------------------------< M A K E _ C O I N S _ D A T E >------------------------------------------------
  171. This function receives a table of date parts for one or two dates and an empty table reference declared in
  172. Module:Citation/CS1. The function is called only for |date= parameters and only if the |date=<value> is
  173. determined to be a valid date format. The question of what to do with invalid date formats is not answered here.
  174. The date parts in the input table are converted to an ISO 8601 conforming date string:
  175. single whole dates: yyyy-mm-dd
  176. month and year dates: yyyy-mm
  177. year dates: yyyy
  178. ranges: yyyy-mm-dd/yyyy-mm-dd
  179. yyyy-mm/yyyy-mm
  180. yyyy/yyyy
  181. Dates in the Julian calendar are reduced to year or year/year so that we don't have to do calendar conversion from
  182. Julian to Proleptic Gregorian.
  183. The input table has:
  184. year, year2 – always present; if before 1582, ignore months and days if present
  185. month, month2 – 0 if not provided, 1-12 for months, 21-24 for seasons; 31– proper name dates
  186. day, day2 – 0 if not provided, 1-31 for days
  187. the output table receives:
  188. rftdate: an IS8601 formatted date
  189. rftchron: a free-form version of the date, usually without year which is in rftdate (season ranges and propername dates)
  190. rftssn: one of four season keywords: winter, spring, summer, fall (lowercase)
  191. ]]
  192. local function make_COinS_date (input, tCOinS_date)
  193. local date; -- one date or first date in a range
  194. local date2 = ''; -- end of range date
  195. if 1582 > tonumber(input.year) or 20 < tonumber(input.month) then -- Julian calendar or season so &rft.date gets year only
  196. date = input.year;
  197. if 0 ~= input.year2 and input.year ~= input.year2 then -- if a range, only the second year portion when not the same as range start year
  198. date = string.format ('%.4d/%.4d', tonumber(input.year), tonumber(input.year2)) -- assemble the date range
  199. end
  200. if 20 < tonumber(input.month) then -- if season or propername date
  201. local season = {[21]='winter', [22]='spring', [23]='summer', [24]='fall', [31]='Christmas'}; -- seasons lowercase, no autumn; proper names use title case
  202. if 0 == input.month2 then -- single season date
  203. if 30 <tonumber(input.month) then
  204. tCOinS_date.rftchron = season[input.month]; -- proper name dates
  205. else
  206. tCOinS_date.rftssn = season[input.month]; -- seasons
  207. end
  208. else -- season range with a second season specified
  209. if input.year ~= input.year2 then -- season year – season year range or season year–year
  210. tCOinS_date.rftssn = season[input.month]; -- start of range season; keep this?
  211. if 0~= month2 then
  212. tCOinS_date.rftchron = string.format ('%s %s – %s %s', season[input.month], input.year, season[input.month2], input.year2);
  213. end
  214. else -- season–season year range
  215. tCOinS_date.rftssn = season[input.month]; -- start of range season; keep this?
  216. tCOinS_date.rftchron = season[input.month] .. '–' .. season[input.month2]; -- season–season year range
  217. end
  218. end
  219. end
  220. tCOinS_date.rftdate = date;
  221. return; -- done
  222. end
  223. if 0 ~= input.day then
  224. date = string.format ('%s-%.2d-%.2d', input.year, tonumber(input.month), tonumber(input.day)); -- whole date
  225. elseif 0 ~= input.month then
  226. date = string.format ('%s-%.2d', input.year, tonumber(input.month)); -- year and month
  227. else
  228. date = string.format ('%s', input.year); -- just year
  229. end
  230. if 0 ~= input.year2 then
  231. if 0 ~= input.day2 then
  232. date2 = string.format ('/%s-%.2d-%.2d', input.year2, tonumber(input.month2), tonumber(input.day2)); -- whole date
  233. elseif 0 ~= input.month2 then
  234. date2 = string.format ('/%s-%.2d', input.year2, tonumber(input.month2)); -- year and month
  235. else
  236. date2 = string.format ('/%s', input.year2); -- just year
  237. end
  238. end
  239. tCOinS_date.rftdate = date .. date2; -- date2 has the '/' separator
  240. return;
  241. end
  242. --[[--------------------------< C H E C K _ D A T E >----------------------------------------------------------
  243. Check date format to see that it is one of the formats approved by WP:DATESNO or WP:DATERANGE. Exception: only
  244. allowed range separator is endash. Additionally, check the date to see that it is a real date: no 31 in 30-day
  245. months; no 29 February when not a leap year. Months, both long-form and three character abbreviations, and seasons
  246. must be spelled correctly. Future years beyond next year are not allowed.
  247. If the date fails the format tests, this function returns false and does not return values for anchor_year and
  248. COinS_date. When this happens, the date parameter is used in the COinS metadata and the CITEREF identifier gets
  249. its year from the year parameter if present otherwise CITEREF does not get a date value.
  250. Inputs:
  251. date_string - date string from date-holding parameters (date, year, accessdate, embargo, archivedate, etc.)
  252. Returns:
  253. false if date string is not a real date; else
  254. true, anchor_year, COinS_date
  255. anchor_year can be used in CITEREF anchors
  256. COinS_date is ISO 8601 format date; see make_COInS_date()
  257. ]]
  258. local function check_date (date_string, tCOinS_date)
  259. local year; -- assume that year2, months, and days are not used;
  260. local year2=0; -- second year in a year range
  261. local month=0;
  262. local month2=0; -- second month in a month range
  263. local day=0;
  264. local day2=0; -- second day in a day range
  265. local anchor_year;
  266. local coins_date;
  267. if date_string:match("^%d%d%d%d%-%d%d?%-%d%d?$") then -- year-initial numerical year month day format
  268. year, month, day=string.match(date_string, "(%d%d%d%d)%-(%d%d?)%-(%d%d?)");
  269. if 12 < tonumber(month) or 1 > tonumber(month) or 1583 > tonumber(year) then return false; end -- month number not valid or not Gregorian calendar
  270. anchor_year = year;
  271. elseif date_string:match("^%a+ +[1-9]%d?, +[1-9]%d%d%d%a?$") then -- month-initial: month day, year
  272. month, day, anchor_year, year=string.match(date_string, "(%a+)%s*(%d%d?),%s*((%d%d%d%d)%a?)");
  273. month = get_month_number (month);
  274. if 0 == month then return false; end -- return false if month text isn't one of the twelve months
  275. elseif date_string:match("^%a+ +[1-9]%d?–[1-9]%d?, +[1-9]%d%d%d%a?$") then -- month-initial day range: month day–day, year; days are separated by endash
  276. month, day, day2, anchor_year, year=string.match(date_string, "(%a+) +(%d%d?)–(%d%d?), +((%d%d%d%d)%a?)");
  277. if tonumber(day) >= tonumber(day2) then return false; end -- date range order is left to right: earlier to later; dates may not be the same;
  278. month = get_month_number (month);
  279. if 0 == month then return false; end -- return false if month text isn't one of the twelve months
  280. month2=month; -- for metadata
  281. year2=year;
  282. elseif date_string:match("^[1-9]%d? +%a+ +[1-9]%d%d%d%a?$") then -- day-initial: day month year
  283. day, month, anchor_year, year=string.match(date_string, "(%d%d*)%s*(%a+)%s*((%d%d%d%d)%a?)");
  284. month = get_month_number (month);
  285. if 0 == month then return false; end -- return false if month text isn't one of the twelve months
  286. elseif date_string:match("^[1-9]%d?–[1-9]%d? +%a+ +[1-9]%d%d%d%a?$") then -- day-range-initial: day–day month year; days are separated by endash
  287. day, day2, month, anchor_year, year=string.match(date_string, "(%d%d?)–(%d%d?) +(%a+) +((%d%d%d%d)%a?)");
  288. if tonumber(day) >= tonumber(day2) then return false; end -- date range order is left to right: earlier to later; dates may not be the same;
  289. month = get_month_number (month);
  290. if 0 == month then return false; end -- return false if month text isn't one of the twelve months
  291. month2=month; -- for metadata
  292. year2=year;
  293. elseif date_string:match("^[1-9]%d? +%a+ – [1-9]%d? +%a+ +[1-9]%d%d%d%a?$") then -- day initial month-day-range: day month - day month year; uses spaced endash
  294. day, month, day2, month2, anchor_year, year=date_string:match("(%d%d?) +(%a+) – (%d%d?) +(%a+) +((%d%d%d%d)%a?)");
  295. if (not is_valid_month_season_range(month, month2)) or not is_valid_year(year) then return false; end -- date range order is left to right: earlier to later;
  296. month = get_month_number (month); -- for metadata
  297. month2 = get_month_number (month2);
  298. year2=year;
  299. elseif date_string:match("^%a+ +[1-9]%d? – %a+ +[1-9]%d?, +[1-9]%d%d%d?%a?$") then -- month initial month-day-range: month day – month day, year; uses spaced endash
  300. month, day, month2, day2, anchor_year, year=date_string:match("(%a+) +(%d%d?) – (%a+) +(%d%d?), +((%d%d%d%d)%a?)");
  301. if (not is_valid_month_season_range(month, month2)) or not is_valid_year(year) then return false; end
  302. month = get_month_number (month); -- for metadata
  303. month2 = get_month_number (month2);
  304. year2=year;
  305. elseif date_string:match("^[1-9]%d? +%a+ +[1-9]%d%d%d – [1-9]%d? +%a+ +[1-9]%d%d%d%a?$") then -- day initial month-day-year-range: day month year - day month year; uses spaced endash
  306. day, month, year, day2, month2, anchor_year, year2=date_string:match("(%d%d?) +(%a+) +(%d%d%d%d?) – (%d%d?) +(%a+) +((%d%d%d%d?)%a?)");
  307. if tonumber(year2) <= tonumber(year) then return false; end -- must be sequential years, left to right, earlier to later
  308. if not is_valid_year(year2) or not is_valid_month_range_style(month, month2) then return false; end -- year2 no more than one year in the future; months same style
  309. month = get_month_number (month); -- for metadata
  310. month2 = get_month_number (month2);
  311. elseif date_string:match("^%a+ +[1-9]%d?, +[1-9]%d%d%d – %a+ +[1-9]%d?, +[1-9]%d%d%d%a?$") then -- month initial month-day-year-range: month day, year – month day, year; uses spaced endash
  312. month, day, year, month2, day2, anchor_year, year2=date_string:match("(%a+) +(%d%d?), +(%d%d%d%d) – (%a+) +(%d%d?), +((%d%d%d%d)%a?)");
  313. if tonumber(year2) <= tonumber(year) then return false; end -- must be sequential years, left to right, earlier to later
  314. if not is_valid_year(year2) or not is_valid_month_range_style(month, month2) then return false; end -- year2 no more than one year in the future; months same style
  315. month = get_month_number (month); -- for metadata
  316. month2 = get_month_number (month2);
  317. elseif date_string:match("^%a+ +[1-9]%d%d%d–%d%d%a?$") then -- special case Winter/Summer year-year (YYYY-YY); year separated with unspaced endash
  318. local century;
  319. month, year, century, anchor_year, year2=date_string:match("(%a+) +((%d%d)%d%d)–((%d%d)%a?)");
  320. if 'Winter' ~= month and 'Summer' ~= month then return false end; -- 'month' can only be Winter or Summer
  321. anchor_year=year..'–'..anchor_year; -- assemble anchor_year from both years
  322. year2 = century..year2; -- add the century to year2 for comparisons
  323. if 1 ~= tonumber(year2) - tonumber(year) then return false; end -- must be sequential years, left to right, earlier to later
  324. if not is_valid_year(year2) then return false; end -- no year farther in the future than next year
  325. month = get_season_number (month);
  326. elseif date_string:match("^%a+ +[1-9]%d%d%d–[1-9]%d%d%d%a?$") then -- special case Winter/Summer year-year; year separated with unspaced endash
  327. month, year, anchor_year, year2=date_string:match("(%a+) +(%d%d%d%d)–((%d%d%d%d)%a?)");
  328. if 'Winter' ~= month and 'Summer' ~= month then return false end; -- 'month' can only be Winter or Summer
  329. anchor_year=year..'–'..anchor_year; -- assemble anchor_year from both years
  330. if 1 ~= tonumber(year2) - tonumber(year) then return false; end -- must be sequential years, left to right, earlier to later
  331. if not is_valid_year(year2) then return false; end -- no year farther in the future than next year
  332. month = get_season_number (month); -- for metadata
  333. elseif date_string:match("^%a+ +[1-9]%d%d%d% – %a+ +[1-9]%d%d%d%a?$") then -- month/season year - month/season year; separated by spaced endash
  334. month, year, month2, anchor_year, year2=date_string:match("(%a+) +(%d%d%d%d) – (%a+) +((%d%d%d%d)%a?)");
  335. anchor_year=year..'–'..anchor_year; -- assemble anchor_year from both years
  336. if tonumber(year) >= tonumber(year2) then return false; end -- left to right, earlier to later, not the same
  337. if not is_valid_year(year2) then return false; end -- no year farther in the future than next year
  338. if 0 ~= get_month_number(month) and 0 ~= get_month_number(month2) and is_valid_month_range_style(month, month2) then -- both must be month year, same month style
  339. month = get_month_number(month);
  340. month2 = get_month_number(month2);
  341. elseif 0 ~= get_season_number(month) and 0 ~= get_season_number(month2) then -- both must be or season year, not mixed
  342. month = get_season_number(month);
  343. month2 = get_season_number(month2);
  344. else
  345. return false;
  346. end
  347. elseif date_string:match ("^%a+–%a+ +[1-9]%d%d%d%a?$") then -- month/season range year; months separated by endash
  348. month, month2, anchor_year, year=date_string:match ("(%a+)–(%a+)%s*((%d%d%d%d)%a?)");
  349. if (not is_valid_month_season_range(month, month2)) or (not is_valid_year(year)) then return false; end
  350. if 0 ~= get_month_number(month) then -- determined to be a valid range so just check this one to know if month or season
  351. month = get_month_number(month);
  352. month2 = get_month_number(month2);
  353. else
  354. month = get_season_number(month);
  355. month2 = get_season_number(month2);
  356. end
  357. year2=year;
  358. elseif date_string:match("^%a+ +%d%d%d%d%a?$") then -- month/season year or proper-name year
  359. month, anchor_year, year=date_string:match("(%a+)%s*((%d%d%d%d)%a?)");
  360. if not is_valid_year(year) then return false; end
  361. if not is_valid_month_or_season (month) and 0 == is_proper_name (month) then return false; end
  362. if 0 ~= get_month_number(month) then -- determined to be a valid range so just check this one to know if month or season
  363. month = get_month_number(month);
  364. elseif 0 ~= get_season_number(month) then
  365. month = get_season_number(month);
  366. else
  367. month = is_proper_name (month); -- must be proper name; not supported in COinS
  368. end
  369. elseif date_string:match("^[1-9]%d%d%d?–[1-9]%d%d%d?%a?$") then -- Year range: YYY-YYY or YYY-YYYY or YYYY–YYYY; separated by unspaced endash; 100-9999
  370. year, anchor_year, year2=date_string:match("(%d%d%d%d?)–((%d%d%d%d?)%a?)");
  371. anchor_year=year..'–'..anchor_year; -- assemble anchor year from both years
  372. if tonumber(year) >= tonumber(year2) then return false; end -- left to right, earlier to later, not the same
  373. if not is_valid_year(year2) then return false; end -- no year farther in the future than next year
  374. elseif date_string:match("^[1-9]%d%d%d–%d%d%a?$") then -- Year range: YYYY–YY; separated by unspaced endash
  375. local century;
  376. year, century, anchor_year, year2=date_string:match("((%d%d)%d%d)–((%d%d)%a?)");
  377. anchor_year=year..'–'..anchor_year; -- assemble anchor year from both years
  378. if 13 > tonumber(year2) then return false; end -- don't allow 2003-05 which might be May 2003
  379. year2 = century..year2; -- add the century to year2 for comparisons
  380. if tonumber(year) >= tonumber(year2) then return false; end -- left to right, earlier to later, not the same
  381. if not is_valid_year(year2) then return false; end -- no year farther in the future than next year
  382. elseif date_string:match("^[1-9]%d%d%d?%a?$") then -- year; here accept either YYY or YYYY
  383. anchor_year, year=date_string:match("((%d%d%d%d?)%a?)");
  384. if false == is_valid_year(year) then
  385. return false;
  386. end
  387. -- LOCAL: do not use mw.ustring: it allows full-width characters for %d.
  388. elseif date_string:match("^[1-9]%d%d%d年[1-9]%d?月[1-9]%d?日$") then -- zh: year month day
  389. year, month, day=date_string:match("(%d%d%d%d)年(%d%d*月)(%d%d*)日");
  390. month = get_month_number (month);
  391. if 0 == month then return false; end
  392. anchor_year = year;
  393. elseif date_string:match("^[1-9]%d%d%d年[1-9]%d?月$") then -- zh: year month
  394. year, month=date_string:match("(%d%d%d%d)年(%d%d*月)");
  395. month = get_month_number (month);
  396. if 0 == month then return false; end
  397. anchor_year = year;
  398. elseif date_string:match("^[1-9]%d%d%d?年$") then -- zh: year; here accept either YYY or YYYY
  399. year=date_string:match("(%d%d%d%d?)年");
  400. if false == is_valid_year(year) then
  401. return false;
  402. end
  403. anchor_year = year;
  404. elseif date_string:match("^%d%d%d%d%-%d%d$") then -- numerical year month format
  405. year, month=date_string:match("(%d%d%d%d)%-(%d%d)");
  406. month=tonumber(month);
  407. if 12 < month or 1 > month or 1583 > tonumber(year) then return false; end -- month number not valid or not Gregorian calendar
  408. anchor_year = year;
  409. -- END LOCAL
  410. else
  411. return false; -- date format not one of the MOS:DATE approved formats
  412. end
  413. local result=true; -- check whole dates for validity; assume true because not all dates will go through this test
  414. if 0 ~= year and 0 ~= month and 0 ~= day and 0 == year2 and 0 == month2 and 0 == day2 then -- YMD (simple whole date)
  415. result=is_valid_date(year,month,day);
  416. elseif 0 ~= year and 0 ~= month and 0 ~= day and 0 == year2 and 0 == month2 and 0 ~= day2 then -- YMD-d (day range)
  417. result=is_valid_date(year,month,day);
  418. result=result and is_valid_date(year,month,day2);
  419. elseif 0 ~= year and 0 ~= month and 0 ~= day and 0 == year2 and 0 ~= month2 and 0 ~= day2 then -- YMD-md (day month range)
  420. result=is_valid_date(year,month,day);
  421. result=result and is_valid_date(year,month2,day2);
  422. elseif 0 ~= year and 0 ~= month and 0 ~= day and 0 ~= year2 and 0 ~= month2 and 0 ~= day2 then -- YMD-ymd (day month year range)
  423. result=is_valid_date(year,month,day);
  424. result=result and is_valid_date(year2,month2,day2);
  425. end
  426. if false == result then return false; end
  427. if nil ~= tCOinS_date then -- this table only passed into this function when testing |date= parameter values
  428. make_COinS_date ({year=year, month=month, day=day, year2=year2, month2=month2, day2=day2}, tCOinS_date); -- make an ISO 8601 date string for COinS
  429. end
  430. return true, anchor_year; -- format is good and date string represents a real date
  431. end
  432. --[[--------------------------< D A T E S >--------------------------------------------------------------------
  433. Cycle the date-holding parameters in passed table date_parameters_list through check_date() to check compliance with MOS:DATE. For all valid dates, check_date() returns
  434. true. The |date= parameter test is unique, it is the only date holding parameter from which values for anchor_year (used in CITEREF identifiers) and COinS_date (used in
  435. the COinS metadata) are derived. The |date= parameter is the only date-holding parameter that is allowed to contain the no-date keywords "n.d." or "nd" (without quotes).
  436. Unlike most error messages created in this module, only one error message is created by this function. Because all of the date holding parameters are processed serially,
  437. a single error message is created as the dates are tested.
  438. ]]
  439. local function dates(date_parameters_list, tCOinS_date)
  440. local anchor_year; -- will return as nil if the date being tested is not |date=
  441. local COinS_date; -- will return as nil if the date being tested is not |date=
  442. local error_message = "";
  443. local good_date = false;
  444. for k, v in pairs(date_parameters_list) do -- for each date-holding parameter in the list
  445. if is_set(v) then -- if the parameter has a value
  446. if v:match("^c%. [1-9]%d%d%d?%a?$") then -- special case for c. year or with or without CITEREF disambiguator - only |date= and |year=
  447. local year = v:match("c%. ([1-9]%d%d%d?)%a?"); -- get the year portion so it can be tested
  448. if 'date'==k then
  449. anchor_year, COinS_date = v:match("((c%. [1-9]%d%d%d?)%a?)"); -- anchor year and COinS_date only from |date= parameter
  450. good_date = is_valid_year(year);
  451. elseif 'year'==k then
  452. good_date = is_valid_year(year);
  453. end
  454. elseif 'date'==k then -- if the parameter is |date=
  455. if v:match("^n%.d%.%a?") then -- if |date=n.d. with or without a CITEREF disambiguator
  456. good_date, anchor_year, COinS_date = true, v:match("((n%.d%.)%a?)"); --"n.d."; no error when date parameter is set to no date
  457. elseif v:match("^nd%a?$") then -- if |date=nd with or without a CITEREF disambiguator
  458. good_date, anchor_year, COinS_date = true, v:match("((nd)%a?)"); --"nd"; no error when date parameter is set to no date
  459. else
  460. good_date, anchor_year, COinS_date = check_date (v, tCOinS_date); -- go test the date
  461. end
  462. elseif 'access-date'==k then -- if the parameter is |date=
  463. good_date = check_date (v); -- go test the date
  464. if true == good_date then -- if the date is a valid date
  465. good_date = is_valid_accessdate (v); -- is Wikipedia start date < accessdate < tomorrow's date?
  466. end
  467. else -- any other date-holding parameter
  468. good_date = check_date (v); -- go test the date
  469. end
  470. if false==good_date then -- assemble one error message so we don't add the tracking category multiple times
  471. if is_set(error_message) then -- once we've added the first portion of the error message ...
  472. error_message=error_message .. ", "; -- ... add a comma space separator
  473. end
  474. error_message=error_message .. "&#124;" .. k .. "="; -- add the failed parameter
  475. end
  476. end
  477. end
  478. return anchor_year, error_message; -- and done
  479. end
  480. --[[--------------------------< Y E A R _ D A T E _ C H E C K >------------------------------------------------
  481. Compare the value provided in |year= with the year value(s) provided in |date=. This function returns a numeric value:
  482. 0 - year value does not match the year value in date
  483. 1 - (default) year value matches the year value in date or one of the year values when date contains two years
  484. 2 - year value matches the year value in date when date is in the form YYYY-MM-DD and year is disambiguated (|year=YYYYx)
  485. ]]
  486. local function year_date_check (year_string, date_string)
  487. local year;
  488. local date1;
  489. local date2;
  490. local result = 1; -- result of the test; assume that the test passes
  491. year = year_string:match ('(%d%d%d%d?)');
  492. if date_string:match ('%d%d%d%d%-%d%d%-%d%d') and year_string:match ('%d%d%d%d%a') then --special case where date and year required YYYY-MM-DD and YYYYx
  493. date1 = date_string:match ('(%d%d%d%d)');
  494. year = year_string:match ('(%d%d%d%d)');
  495. if year ~= date1 then
  496. result = 0; -- years don't match
  497. else
  498. result = 2; -- years match; but because disambiguated, don't add to maint cat
  499. end
  500. elseif date_string:match ("%d%d%d%d?.-%d%d%d%d?") then -- any of the standard formats of date with two three- or four-digit years
  501. date1, date2 = date_string:match ("(%d%d%d%d?).-(%d%d%d%d?)");
  502. if year ~= date1 and year ~= date2 then
  503. result = 0;
  504. end
  505. elseif date_string:match ("%d%d%d%d[%s%-–]+%d%d") then -- YYYY-YY date ranges
  506. local century;
  507. date1, century, date2 = date_string:match ("((%d%d)%d%d)[%s%-–]+(%d%d)");
  508. date2 = century..date2; -- convert YY to YYYY
  509. if year ~= date1 and year ~= date2 then
  510. result = 0;
  511. end
  512. elseif date_string:match ("%d%d%d%d?") then -- any of the standard formats of date with one year
  513. date1 = date_string:match ("(%d%d%d%d?)");
  514. if year ~= date1 then
  515. result = 0;
  516. end
  517. end
  518. return result;
  519. end
  520. return {dates = dates, year_date_check = year_date_check} -- return exported functions