پودمان:Citation/CS1/fa/date-i18n
ظاهر
توضیحات این پودمان میتواند در پودمان:Citation/CS1/fa/date-i18n/توضیحات قرار گیرد.
--[[--------------------------< Module:Citation/CS1/fa/date-i18n >--------------------------------------------
Persian (fa) date helper for the verbatim-en Date_validation engine. All Farsi/Jalali date logic lives HERE;
the engine calls into it at a few clearly-marked hook points so the engine stays drop-in updatable.
Jalali (solar hijri) years run ~1210–1450 (≈1831–2071 CE); months 1–6 have 31 days, 7–11 have 30, 12 has 29/30.
]]
local p = {}
local western = {['۰']='0',['۱']='1',['۲']='2',['۳']='3',['۴']='4',['۵']='5',['۶']='6',['۷']='7',['۸']='8',['۹']='9',
['٠']='0',['١']='1',['٢']='2',['٣']='3',['٤']='4',['٥']='5',['٦']='6',['٧']='7',['٨']='8',['٩']='9'}
function p.to_western (s) -- normalize Persian/Arabic-Indic digits to Western 0-9
if not s then return s end
return (mw.ustring.gsub (s, '[۰-۹٠-٩]', western));
end
function p.is_jalali_year (y) -- heuristic: a year in the Jalali range (distinct from Gregorian ≥1582)
y = tonumber (p.to_western (tostring (y)));
return y ~= nil and y >= 1210 and y <= 1450;
end
local jalali_days = {31,31,31,31,31,31,30,30,30,30,30,30};
function p.jalali_valid (y, m, d) -- validate a numeric Jalali Y/M/D
y = tonumber (p.to_western (tostring (y)));
m = tonumber (p.to_western (tostring (m)));
d = tonumber (p.to_western (tostring (d)));
if not (y and m and d) then return false; end
if m < 1 or m > 12 or d < 1 then return false; end
local len = jalali_days[m];
if 12 == m then len = 30; end -- اسفند: 29 common, 30 leap; accept ≤30
return d <= len;
end
p.solar_long = {['فروردین']=1,['اردیبهشت']=2,['خرداد']=3,['تیر']=4,['مرداد']=5,['شهریور']=6,
['مهر']=7,['آبان']=8,['آذر']=9,['دی']=10,['بهمن']=11,['اسفند']=12,
-- نامهای ماههای خورشیدی افغانستان/دری (همان تقویم هجری خورشیدی)
['حمل']=1,['ثور']=2,['جوزا']=3,['سرطان']=4,['اسد']=5,['سنبله']=6,
['میزان']=7,['عقرب']=8,['قوس']=9,['جدی']=10,['دلو']=11,['حوت']=12};
local _eastern = {['0']='۰',['1']='۱',['2']='۲',['3']='۳',['4']='۴',['5']='۵',['6']='۶',['7']='۷',['8']='۸',['9']='۹',
['٠']='۰',['١']='۱',['٢']='۲',['٣']='۳',['٤']='۴',['٥']='۵',['٦']='۶',['٧']='۷',['٨']='۸',['٩']='۹'} -- Western + Arabic-Indic → Persian
function p.to_fa_digits (s) -- normalize Arabic-Indic digits (٠-٩) to Persian (۰-۹); Western/Persian and non-digits pass through unchanged
if not s then return s end
return (mw.ustring.gsub (s, '[٠-٩]', _eastern));
end
-- Convert Western digits to Persian in VISIBLE citation text (operates on citation WIKITEXT). Skips wikilinks
-- [[...]], external links [...], HTML tags <...>/<a>...</a>, and entities &...; so identifiers (ISBN/DOI),
-- URLs, CSS classes and   keep their Western digits.
function p.digits_fa (s)
if not s then return s end
s = p.to_fa_digits (s); -- fa: Arabic-Indic → Persian first (char-aware; the byte-loop below handles ASCII, and skips links/tags)
local out, i, n = {}, 1, #s
while i <= n do
local c = s:sub(i,i)
if c == '[' and s:sub(i+1,i+1) == '[' then
local k = s:find(']]', i+2, true)
if k then out[#out+1] = s:sub(i, k+1); i = k+2 else out[#out+1] = s:sub(i); i = n+1 end
elseif c == '[' then
local k = s:find(']', i+1, true)
if k then out[#out+1] = s:sub(i, k); i = k+1 else out[#out+1] = c; i = i+1 end
elseif c == '<' then
local j = s:find('>', i, true) or n
if s:sub(i, i+2):lower():match('^<a[%s>/]') then
local k = s:find('</a>', j, true)
if k then out[#out+1] = s:sub(i, k+3); i = k+4 else out[#out+1] = s:sub(i); i = n+1 end
else out[#out+1] = s:sub(i, j); i = j+1 end
elseif c == '&' then
local j = s:find(';', i, true) or i
out[#out+1] = s:sub(i, j); i = j+1
elseif _eastern[c] then out[#out+1] = _eastern[c]; i = i+1
else out[#out+1] = c; i = i+1 end
end
return table.concat(out)
end
p.seasons = {['بهار'] = 21, ['تابستان'] = 22, ['پاییز'] = 23, ['زمستان'] = 24}; -- fa: season order (matches date_names.local.season); used for ordered season PAIRS
function p.check_solar (s) -- parse a Jalali date with a SOLAR MONTH NAME: «۱۰ شهریور ۱۳۹۱» / «شهریور ۱۳۹۱» / range «خرداد–تیر ۱۳۸۷» / «خرداد و تیر ۱۳۸۷»; return the year (original script) if valid, else nil
if not s then return nil end
s = mw.text.trim (s);
local d, mon, y = mw.ustring.match (s, '^([%d۰-۹]+)%s+(%S+)%s+([%d۰-۹]+)$'); -- day month-name year
if not d then
mon, y = mw.ustring.match (s, '^(%S+)%s+([%d۰-۹]+)$'); -- month-name year
d = '1';
end
if mon and y and p.solar_long[mon] and p.is_jalali_year (y) and p.jalali_valid (y, p.solar_long[mon], d) then
return y; -- valid single Jalali date
end
local m1, m2, ry = mw.ustring.match (s, '^(%S+)%s*[–%-]%s*(%S+)%s+([%d۰-۹]+)$'); -- range: month1–month2 year (double issue)
if not m1 then m1, m2, ry = mw.ustring.match (s, '^(%S+)%s+و%s+(%S+)%s+([%d۰-۹]+)$'); end -- «month1 و month2 year»
if not m1 then m1, m2, ry = mw.ustring.match (s, '^(%S+)%s+تا%s+(%S+)%s+([%d۰-۹]+)$'); end -- «month1 تا month2 year»
if m1 and p.solar_long[m1] and p.solar_long[m2] and p.is_jalali_year (ry) then
return ry; -- valid Jalali month-range (both solar months, same year)
end
if m1 and p.seasons[m1] and p.seasons[m2] and p.is_jalali_year (ry) then -- season PAIR (biannual issue): «season1 و/تا/– season2 year»
if p.seasons[m1] < p.seasons[m2] then return ry; end -- accept ONLY ascending order (بهار<تابستان<پاییز<زمستان)
return nil; -- out-of-order or same season → reject → engine emits date error
end
local sm1, sy1, sm2, sy2 = mw.ustring.match (s, '^(%S+)%s+([%d۰-۹]+)%s+و%s+(%S+)%s+([%d۰-۹]+)$'); -- season pair spanning two years: «season1 year1 و season2 year2»
if not sm1 then sm1, sy1, sm2, sy2 = mw.ustring.match (s, '^(%S+)%s+([%d۰-۹]+)%s+تا%s+(%S+)%s+([%d۰-۹]+)$'); end -- «season1 year1 تا season2 year2»
if not sm1 then sm1, sy1, sm2, sy2 = mw.ustring.match (s, '^(%S+)%s+([%d۰-۹]+)%s*[–%-]%s*(%S+)%s+([%d۰-۹]+)$'); end -- «season1 year1–season2 year2»
if sm1 and p.seasons[sm1] and p.seasons[sm2] and p.is_jalali_year (sy1) and p.is_jalali_year (sy2) then -- both must be valid seasons, both Jalali years
if tonumber (p.to_western (sy2)) == tonumber (p.to_western (sy1)) + 1 then -- accept ONLY consecutive years («زمستان ۱۳۷۴ و بهار ۱۳۷۵»); «… ۱۳۷۶» rejected
return sy1; -- valid cross-year season pair; anchor year = first year
end
return nil; -- non-consecutive years → reject → engine emits date error
end
return nil; -- not a recognized solar date → let the engine handle the string
end
-- Accept variant Gregorian month spellings the old fa module recognized (Afghan/informal) → the standard
-- MediaWiki fa name, so validation succeeds; only affects the validated copy, not the displayed date.
local _month_variants = {['آگوست']='اوت', ['جولای']='ژوئیه', ['جون']='ژوئن', ['می']='مه'};
function p.normalize_numeric_slash (s) -- fa: «۱۴۰۵/۰۳/۲۱» (Jalali numeric YYYY/MM/DD, slash-separated) → hyphen form «۱۴۰۵-۰۳-۲۱» so the verbatim-en ymd validator accepts it; ONLY when the year is Jalali (Gregorian slash dates stay invalid per MOS:DATE) and only the zero-padded 4-2-2 shape (parity with the hyphen ymd pattern)
if not s then return s end
local D = '[%d۰-۹]';
local y = mw.ustring.match (s, '^(' .. D .. D .. D .. D .. ')/' .. D .. D .. '/' .. D .. D .. '$');
if y and p.is_jalali_year (y) then
return (mw.ustring.gsub (s, '/', '-'));
end
return s;
end
function p.norm_months (s)
if not s then return s end
s = p.normalize_numeric_slash (s); -- fa: Jalali numeric slash date «۱۴۰۵/۰۳/۲۱» → hyphen form so the verbatim-en ymd validator accepts it
s = mw.ustring.gsub (s, 'ۀ', 'ه'); -- fa: precomposed heh-with-hamza (U+06C0) → heh
s = mw.ustring.gsub (s, 'ٔ', ''); -- fa: strip ezafe hamza (U+0654) so «ژانویهٔ»/«مهٔ» match «ژانویه»/«مه» in dates
for a, b in pairs (_month_variants) do
s = mw.ustring.gsub (s, a, b);
end
return s;
end
return p