پرش به محتوا

پودمان: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 &#160; 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