モジュール:AgeEstimator
このモジュールについての説明文ページを モジュール:AgeEstimator/doc に作成できます
-- Module:AgeEstimator
local p = {}
local function trim(s)
if s == nil then return '' end
s = tostring(s)
return mw.ustring.gsub(s, '^%s*(.-)%s*$', '%1')
end
-- 実質的な情報を含まない値(プレースホルダー)かどうか
local function isPlaceholder(s)
s = trim(s)
if s == '' then return true end
if mw.ustring.match(s, '^[%-_%s%.•・、。,,.-ー–—// ]+$') then
return true
end
local lower = mw.ustring.lower(s)
local nolist = {'不明','なし','無し','未知','未定','未詳','不詳',
'unknown','n/a','none','tbd','null','nil'}
for _, w in ipairs(nolist) do
if lower == w then return true end
end
return false
end
local APPROX = {
'前後','頃','ごろ','くらい','ぐらい','弱','強','約',
'およそ','大体','ほぼ','辺り','あたり','付近',
'大约','约','左右','前后','上下',
'약','정도','쯤','경',
'примерно','около','приблизительно','почти','с лишним',
'approximately','around','about','circa','-ish','or so',
'حوالي','نحو','قرابة','تقريبا',
}
local EARLY = {
'初め','初期','初頭','前半','上期','上半期','上半','前段','前叶','前葉',
'초반','초기','상반기',
'начало','начале','первая','первой','ранн',
'early','first','1st',
'المبكر','الأول',
}
local MID = {
'中頃','中ごろ','半ば','中盤','中期','中葉',
'중반','중기',
'середина','середине','средн',
'mid','middle',
'الأوسط','منتصف',
}
local LATE = {
'終わり','末期','終期','晩期','最末期','末','後半','下期','下半期','下半','後期',
'后期','末期','末','后半',
'후반','후기','하반기',
'конец','конце','конца','поздн','последн','вторая','второй',
'late','last','second','2nd',
'المتأخر','الأخير','الثاني',
}
local MONTH_IN = {
january=1, february=2, march=3, april=4, may=5, june=6,
july=7, august=8, september=9, october=10, november=11, december=12,
jan=1, feb=2, mar=3, apr=4, jun=6, jul=7, aug=8,
sep=9, sept=9, oct=10, nov=11, dec=12,
['января']=1, ['февраля']=2, ['марта']=3, ['апреля']=4,
['мая']=5, ['июня']=6, ['июля']=7, ['августа']=8,
['сентября']=9, ['октября']=10, ['ноября']=11, ['декабря']=12,
['январь']=1, ['февраль']=2, ['март']=3, ['апрель']=4,
['май']=5, ['июнь']=6, ['июль']=7, ['август']=8,
['сентябрь']=9, ['октябрь']=10, ['ноябрь']=11, ['декабрь']=12,
['يناير']=1, ['فبراير']=2, ['مارس']=3, ['أبريل']=4, ['ابريل']=4,
['مايو']=5, ['يونيو']=6, ['يوليو']=7,
['أغسطس']=8, ['اغسطس']=8,
['سبتمبر']=9, ['أكتوبر']=10, ['اكتوبر']=10,
['نوفمبر']=11, ['ديسمبر']=12,
}
local MONTH_OUT = {
en = {'January','February','March','April','May','June',
'July','August','September','October','November','December'},
ru = {'января','февраля','марта','апреля','мая','июня',
'июля','августа','сентября','октября','ноября','декабря'},
ar = {'يناير','فبراير','مارس','أبريل','مايو','يونيو',
'يوليو','أغسطس','سبتمبر','أكتوبر','نوفمبر','ديسمبر'},
}
local function containsAny(s, list)
for _, w in ipairs(list) do
if mw.ustring.find(s, w, 1, true) then return true end
end
return false
end
local function findMonthDayByName(s)
local lower_s = mw.ustring.lower(s)
for name, num in pairs(MONTH_IN) do
if mw.ustring.find(lower_s, name, 1, true) then
local d = mw.ustring.match(lower_s, '(%d%d?)%s*' .. name)
if not d then
d = mw.ustring.match(lower_s, name .. '%s*(%d%d?)')
end
if d then
return num, tonumber(d)
end
end
end
return nil
end
local function normalize(s)
s = tostring(s or '')
s = mw.ustring.gsub(s, '[??]', '')
s = mw.ustring.gsub(s, '[0-9]', function(c)
return string.char(string.byte(c) - 0xFEE0)
end)
local map = {
['٠']='0',['١']='1',['٢']='2',['٣']='3',['٤']='4',
['٥']='5',['٦']='6',['٧']='7',['٨']='8',['٩']='9',
['۰']='0',['۱']='1',['۲']='2',['۳']='3',['۴']='4',
['۵']='5',['۶']='6',['۷']='7',['۸']='8',['۹']='9',
}
s = mw.ustring.gsub(s, '[٠-٩۰-۹]', function(c) return map[c] end)
s = mw.ustring.gsub(s, ' ', ' ')
s = mw.ustring.gsub(s, '[ー-−―─—–~~〜]', '-')
s = mw.ustring.lower(s)
s = mw.ustring.gsub(s, '%s*から%s*', '-')
s = mw.ustring.gsub(s, '%s*まで%s*', '-')
s = mw.ustring.gsub(s, '%s*至%s*', '-')
s = mw.ustring.gsub(s, '%s*到%s*', '-')
s = mw.ustring.gsub(s, '%s*[Tt][Oo]%s*', '-')
s = mw.ustring.gsub(s, '%s+по%s+', '-')
s = mw.ustring.gsub(s, '%s*[Bb][Ii][Ss]%s+', '-')
s = mw.ustring.gsub(s, '^%s*[Ff][Rr][Oo][Mm]%s+', '')
s = mw.ustring.gsub(s, '^%s*[Bb][Oo][Rr][Nn]%s+(in%s+|on%s+)?', '')
s = mw.ustring.gsub(s, '^%s*[Dd][Ii][Ee][Dd]%s+(in%s+|on%s+)?', '')
s = mw.ustring.gsub(s, '^%s*[Dd][Ee]%s+', '')
s = mw.ustring.gsub(s, '^%s*[Cc][Ii][Rr][Cc][Aa]%s+', '')
s = mw.ustring.gsub(s, '^%s*[Cc][Aa]%.?%s+', '')
s = mw.ustring.gsub(s, '^%s*с%s+', '')
s = mw.ustring.gsub(s, '^%s*от%s+', '')
return mw.text.trim(s)
end
local function extractNumbers(s)
local nums = {}
for n in mw.ustring.gmatch(s, '%d+') do
table.insert(nums, tonumber(n))
end
return nums
end
local function isApproximate(s)
if containsAny(s, APPROX) then return true end
if mw.ustring.find(s, '-', 1, true) then return true end
return false
end
-- 元号(令和の終端は現在年)
local CURRENT_YEAR = tonumber(os.date('%Y')) or 2026
local ERAS = {
{'明治', 1868, 1912},
{'大正', 1912, 1926},
{'昭和', 1926, 1989},
{'平成', 1989, 2019},
{'令和', 2019, CURRENT_YEAR},
}
local function parseEra(s)
for _, era in ipairs(ERAS) do
local name, S, E = era[1], era[2], era[3]
if mw.ustring.find(s, name, 1, true) then
local L = E - S
local n = tonumber(mw.ustring.match(s, '(%d+)年'))
if n and n < 100 then
return {year = S + n - 1, exact = false}
end
local dec = tonumber(mw.ustring.match(s, '(%d+)年代'))
if dec and dec < 100 then
return {year = S + dec - 1 + 5, exact = false}
end
if containsAny(s, {'末期','終期','晩期','最末期'}) then
return {year = E - L/12, exact = false}
end
if containsAny(s, EARLY) then
return {year = S + L/6, exact = false}
end
if containsAny(s, MID) then
return {year = S + L/2, exact = false}
end
if containsAny(s, LATE) then
return {year = S + 5*L/6, exact = false}
end
return {year = S + L/2, exact = false}
end
end
return nil
end
local function parseYMD(s)
local y1,m1,d1,y2,m2,d2 = mw.ustring.match(
s, '(%d%d%d%d)年(%d%d?)月(%d%d?)日.-(%d%d%d%d)年(%d%d?)月(%d%d?)日')
if y1 then
return {year=(tonumber(y1)+tonumber(y2))/2,
month=(tonumber(m1)+tonumber(m2))/2,
day=(tonumber(d1)+tonumber(d2))/2, exact=false}
end
local y3,m3,d3,d4 = mw.ustring.match(
s, '(%d%d%d%d)年(%d%d?)月(%d%d?)日.-(%d%d?)日')
if y3 then
return {year=tonumber(y3), month=tonumber(m3),
day=(tonumber(d3)+tonumber(d4))/2, exact=false}
end
local y4,m4,d4b = mw.ustring.match(s, '(%d%d%d%d)年(%d%d?)月(%d%d?)日')
if y4 then return {year=tonumber(y4),month=tonumber(m4),day=tonumber(d4b),exact=true} end
local yk1,mk1,dk1,yk2,mk2,dk2 = mw.ustring.match(
s, '(%d%d%d%d)년%s*(%d%d?)월%s*(%d%d?)일.-(%d%d%d%d)년%s*(%d%d?)월%s*(%d%d?)일')
if yk1 then
return {year=(tonumber(yk1)+tonumber(yk2))/2,
month=(tonumber(mk1)+tonumber(mk2))/2,
day=(tonumber(dk1)+tonumber(dk2))/2, exact=false}
end
local yk,mk,dk = mw.ustring.match(s, '(%d%d%d%d)년%s*(%d%d?)월%s*(%d%d?)일')
if yk then return {year=tonumber(yk),month=tonumber(mk),day=tonumber(dk),exact=true} end
local ya, ma, da = mw.ustring.match(s, '(%d%d%d%d)[%-/%.](%d%d?)[%-/%.](%d%d?)')
if ya then return {year=tonumber(ya),month=tonumber(ma),day=tonumber(da),exact=true} end
local y_named = tonumber(mw.ustring.match(s, '(%d%d%d%d)'))
if y_named then
local mn, dd = findMonthDayByName(s)
if mn and dd then
return {year=y_named, month=mn, day=dd, exact=true}
end
end
local y5,m5 = mw.ustring.match(s, '(%d%d%d%d)年(%d%d?)月')
if y5 then return {year=tonumber(y5),month=tonumber(m5),day=15,exact=false} end
local y5k,m5k = mw.ustring.match(s, '(%d%d%d%d)년%s*(%d%d?)월')
if y5k then return {year=tonumber(y5k),month=tonumber(m5k),day=15,exact=false} end
local y6 = tonumber(mw.ustring.match(s, '(%d%d%d%d)年'))
if y6 then
if containsAny(s, {'上半期','上期','上半年','上半','상반기',
'первая','первой','first half','1st half','النصف الأول'}) then
return {year=y6,month=3,day=15,exact=false}
end
if containsAny(s, {'下半期','下期','下半年','下半','하반기',
'вторая','второй','second half','2nd half','النصف الثاني'}) then
return {year=y6,month=9,day=15,exact=false}
end
return {year=y6,exact=true}
end
local y6k = tonumber(mw.ustring.match(s, '(%d%d%d%d)년'))
if y6k then
if containsAny(s, {'상반기'}) then
return {year=y6k,month=3,day=15,exact=false}
end
if containsAny(s, {'하반기'}) then
return {year=y6k,month=9,day=15,exact=false}
end
return {year=y6k,exact=true}
end
return nil
end
local function parseDecade(s, nums)
local era = parseEra(s)
if era then return era end
if #nums == 0 then return nil end
local off = 5
if containsAny(s, EARLY) then off = 2
elseif containsAny(s, MID) then off = 5
elseif containsAny(s, LATE) then off = 7 end
local base4 = mw.ustring.match(s, '(%d%d%d%d)s')
if base4 then return {year = tonumber(base4) + off, exact = false} end
local base2 = mw.ustring.match(s, '(%d%d)s')
if base2 then return {age = tonumber(base2) + off, exact = false} end
local nendai = mw.ustring.find(s, '年代', 1, true)
or mw.ustring.find(s, '년대', 1, true)
local dai = mw.ustring.find(s, '代', 1, true)
or mw.ustring.find(s, '台', 1, true)
or mw.ustring.find(s, '대', 1, true)
if nendai then
local base = nums[1]
if base < 100 then base = base + 1900 end
return {year = base + off, exact = false}
end
local ru = mw.ustring.match(s, '(%d%d%d%d)[%-]?[ех]')
if ru then return {year = tonumber(ru) + off, exact = false} end
if dai then
local base = nums[1]
if base >= 1900 then return {year = base + off, exact = false} end
if base < 100 then return {age = base + off, exact = false} end
end
return nil
end
local function parseAge(s, nums)
local markers = {'歳','才','살','세','years old','year old','y.o.','yo.',
' سنة','سنةً','عاما','عاماً','عام'}
local found = false
for _, m in ipairs(markers) do
if mw.ustring.find(s, m, 1, true) then found = true; break end
end
if not found then return nil end
if #nums == 0 then return nil end
if #nums >= 2 then
local a,b = nums[1],nums[2]
if a>b then a,b = b,a end
return {age=(a+b)/2, exact=false}
end
local a = nums[1]
if mw.ustring.find(s, '弱', 1, true) then a = a - 1 end
if mw.ustring.find(s, '強', 1, true) then a = a + 1 end
return {age=a, exact=not isApproximate(s)}
end
local function parseExpression(s)
s = normalize(s)
if s == '' then return nil end
local nums = extractNumbers(s)
if #nums == 0 and not mw.ustring.find(s, '昭和', 1, true)
and not mw.ustring.find(s, '平成', 1, true)
and not mw.ustring.find(s, '大正', 1, true)
and not mw.ustring.find(s, '明治', 1, true)
and not mw.ustring.find(s, '令和', 1, true) then
return nil
end
local dec = parseDecade(s, nums); if dec then return dec end
local age = parseAge(s, nums); if age then return age end
local ymd = parseYMD(s); if ymd then return ymd end
if #nums >= 2 then
local a,b = nums[1], nums[2]
if a >= 1000 and a <= 2200 and b < 100 then
local mult = 10 ^ #tostring(b)
b = math.floor(a / mult) * mult + b
end
if a > b then a,b = b,a end
if a >= 1000 and b <= 2200 then
return {year=(a+b)/2, exact=false}
else
return {age=(a+b)/2, exact=false}
end
end
local n = nums[1]
if n >= 1000 and n <= 2200 then
return {year=n, exact=not isApproximate(s)}
else
return {age=n, exact=not isApproximate(s)}
end
end
-- 月日のみを抽出(年なし)
local function parseMonthDay(s)
if s == '' then return nil end
local bm, bd = mw.ustring.match(s, '(%d%d?)月(%d%d?)日')
if bm and bd then
return {month=tonumber(bm), day=tonumber(bd), exact=true}
end
bm, bd = mw.ustring.match(s, '(%d%d?)월%s*(%d%d?)일')
if bm and bd then
return {month=tonumber(bm), day=tonumber(bd), exact=true}
end
bm = mw.ustring.match(s, '(%d%d?)月')
if bm then
return {month=tonumber(bm), day=15, exact=false}
end
bm = mw.ustring.match(s, '(%d%d?)월')
if bm then
return {month=tonumber(bm), day=15, exact=false}
end
local mn, dd = findMonthDayByName(s)
if mn and dd then
return {month=mn, day=dd, exact=true}
end
local nm, nd = mw.ustring.match(s, '^(%d%d?)[%-/%.](%d%d?)$')
if nm and nd then
return {month=tonumber(nm), day=tonumber(nd), exact=true}
end
return nil
end
-- 生年の末尾に付いた「年」「년」を数値のみの場合に限って除去
local function stripYearSuffix(s)
s = trim(s)
if s == '' then return s end
local n = mw.ustring.match(s, '^(%d+)年$') or mw.ustring.match(s, '^(%d+)년$')
if n then return n end
return s
end
local function parseBirth(birth, birth2)
birth = stripYearSuffix(birth)
if birth ~= '' and birth2 == '' then
return parseExpression(birth)
end
if birth == '' and birth2 ~= '' then
return parseMonthDay(birth2)
end
if birth ~= '' and birth2 ~= '' then
local by = tonumber(birth)
if by and by >= 1000 and by <= 2200 then
local bm, bd = mw.ustring.match(birth2, '(%d%d?)月(%d%d?)日')
if bm and bd then
return {year=by, month=tonumber(bm), day=tonumber(bd), exact=true}
end
bm, bd = mw.ustring.match(birth2, '(%d%d?)월%s*(%d%d?)일')
if bm and bd then
return {year=by, month=tonumber(bm), day=tonumber(bd), exact=true}
end
local bm2 = mw.ustring.match(birth2, '(%d%d?)月')
if bm2 then
return {year=by, month=tonumber(bm2), day=15, exact=false}
end
bm2 = mw.ustring.match(birth2, '(%d%d?)월')
if bm2 then
return {year=by, month=tonumber(bm2), day=15, exact=false}
end
local mn, dd = findMonthDayByName(birth2)
if mn and dd then
return {year=by, month=mn, day=dd, exact=true}
end
local nm, nd = mw.ustring.match(birth2, '^(%d%d?)[%-/%.](%d%d?)$')
if nm and nd then
return {year=by, month=tonumber(nm), day=tonumber(nd), exact=true}
end
end
local combined
if mw.ustring.find(birth2,'月') or mw.ustring.find(birth2,'日')
or mw.ustring.find(birth2,'월') or mw.ustring.find(birth2,'일') then
combined = birth .. '年' .. birth2
else
combined = birth .. birth2
end
return parseExpression(combined)
end
return nil
end
local function units(lang)
if lang=='en' then return ' years old',' approx.' end
if lang=='ar' then return ' سنة',' حوالي' end
if lang=='zh' then return '岁','左右' end
if lang=='zh-hant' then return '歲','左右' end
if lang=='ko' then return '살','경' end
if lang=='ru' then return ' лет',' ок.' end
return '歳','頃'
end
local function formatBirthDate(lang, y, m, d)
if m and d then
if lang=='ko' then
return string.format('%d년 %d월 %d일', y, m, d)
elseif lang=='en' then
return MONTH_OUT.en[m] .. ' ' .. d .. ', ' .. y
elseif lang=='ru' then
return d .. ' ' .. MONTH_OUT.ru[m] .. ' ' .. y
elseif lang=='ar' then
return d .. ' ' .. MONTH_OUT.ar[m] .. ' ' .. y
else
return string.format('%d年%d月%d日', y, m, d)
end
else
if lang=='ko' then
return tostring(y) .. '년'
elseif lang=='en' or lang=='ru' or lang=='ar' then
return tostring(y)
else
return tostring(y) .. '年'
end
end
end
local function formatMonthDay(lang, m, d)
if lang=='ko' then
return string.format('%d월 %d일', m, d)
elseif lang=='en' then
return MONTH_OUT.en[m] .. ' ' .. d
elseif lang=='ru' then
return d .. ' ' .. MONTH_OUT.ru[m]
elseif lang=='ar' then
return d .. ' ' .. MONTH_OUT.ar[m]
else
return string.format('%d月%d日', m, d)
end
end
function p.field(frame)
local args = frame:getParent().args
local ageStr = trim(args['年齢'])
local birth = trim(args['生年'])
local birth2 = trim(args['生年2'])
local lang = trim(args['lang']); if lang=='' then lang='ja' end
lang = mw.ustring.lower(lang)
local ageInfo = (ageStr ~= '') and parseExpression(ageStr) or nil
local birthInfo = parseBirth(birth, birth2)
local refY = tonumber(os.date('%Y'))
local refM = tonumber(os.date('%m'))
local refD = tonumber(os.date('%d'))
local finalAge, ageApprox, ageRange = nil, false, nil
local src = ageInfo or birthInfo
if src then
if src.age then
finalAge = src.age
ageApprox = not src.exact
elseif src.year then
finalAge = refY - src.year
if src.month and src.day then
if refM < src.month or (refM == src.month and refD < src.day) then
finalAge = finalAge - 1
end
end
ageApprox = not src.exact
if lang=='ja' and not ageInfo and src.exact
and not src.month and src.year then
local y = math.floor(src.year + 0.5)
local mx = refY - y
ageRange = {min=mx-1, max=mx}
end
end
end
local ageUnit, approxMark = units(lang)
local birthOut = ''
if birthInfo then
if birthInfo.year then
local by = math.floor(birthInfo.year + 0.5)
local bm = birthInfo.month and math.floor(birthInfo.month + 0.5) or nil
local bd = birthInfo.day and math.floor(birthInfo.day + 0.5) or nil
birthOut = formatBirthDate(lang, by, bm, bd)
elseif birthInfo.month and birthInfo.day then
local bm = math.floor(birthInfo.month + 0.5)
local bd = math.floor(birthInfo.day + 0.5)
birthOut = formatMonthDay(lang, bm, bd)
end
if birthOut ~= '' and not birthInfo.exact then
birthOut = birthOut .. approxMark
end
end
if birth ~= '' and birth2 ~= '' and birthInfo and birthInfo.exact
and not birthInfo.month then
if lang=='en' or lang=='ar' or lang=='ru' then
birthOut = birth2 .. ', ' .. birth
elseif lang=='ja' or lang=='zh' or lang=='zh-hant' then
birthOut = birth .. '年' .. birth2
elseif lang=='ko' then
birthOut = birth .. '년 ' .. birth2
end
end
if birthOut == '' and finalAge then
local by = math.floor(refY - finalAge + 0.5)
if lang=='en' or lang=='ru' or lang=='ar' then
birthOut = tostring(by)
elseif lang=='ko' then
birthOut = tostring(by) .. '년'
else
birthOut = tostring(by) .. '年'
end
birthOut = birthOut .. approxMark
end
local ageOut = ''
if ageRange then
ageOut = tostring(ageRange.min)..'~'..tostring(ageRange.max)..ageUnit
elseif finalAge then
local a = math.floor(finalAge + 0.5)
ageOut = tostring(a)..ageUnit
if ageApprox then ageOut = ageOut .. approxMark end
end
if ageInfo == nil and ageStr ~= '' and not isPlaceholder(ageStr) then
ageOut = ageStr
end
if birthInfo == nil and (birth ~= '' or birth2 ~= '') then
local combined
if birth ~= '' and birth2 ~= '' then
combined = birth .. ' ' .. birth2
elseif birth ~= '' then
combined = birth
else
combined = birth2
end
if not isPlaceholder(combined) then
birthOut = combined
end
end
if lang=='en' then
if ageOut~='' and birthOut~='' then return ageOut..', born '..birthOut end
if ageOut~='' then return ageOut end
if birthOut~='' then return 'born '..birthOut end
elseif lang=='ar' then
if ageOut~='' and birthOut~='' then return ageOut..'، ولد في '..birthOut end
if ageOut~='' then return ageOut end
if birthOut~='' then return 'ولد في '..birthOut end
elseif lang=='zh' or lang=='zh-hant' then
if ageOut~='' and birthOut~='' then return ageOut..','..birthOut..'生' end
if ageOut~='' then return ageOut end
if birthOut~='' then return birthOut..'生' end
elseif lang=='ko' then
if ageOut~='' and birthOut~='' then return ageOut..', '..birthOut..'생' end
if ageOut~='' then return ageOut end
if birthOut~='' then return birthOut..'생' end
elseif lang=='ru' then
if ageOut~='' and birthOut~='' then return ageOut..', род. '..birthOut end
if ageOut~='' then return ageOut end
if birthOut~='' then return 'род. '..birthOut end
else
if ageOut~='' and birthOut~='' then return ageOut..'、'..birthOut..'生まれ' end
if ageOut~='' then return ageOut end
if birthOut~='' then return birthOut..'生まれ' end
end
return ''
end
function p.hasdata(frame)
local args = frame:getParent().args
local ageStr = trim(args['年齢'])
local birth = trim(args['生年'])
local birth2 = trim(args['生年2'])
if ageStr ~= '' and not isPlaceholder(ageStr) then return 'x' end
if birth ~= '' and not isPlaceholder(birth) then return 'x' end
if birth2 ~= '' and not isPlaceholder(birth2) then return 'x' end
return ''
end
return p