모듈:음양력
이 모듈에 대한 설명문서는 모듈:음양력/설명문서에서 만들 수 있습니다
--[[
Module:음양력 (Lunisolar <-> Gregorian calendar converter)
천문 계산(삭·중기)으로 태음태양력(음력)과 그레고리력(양력)을 변환한다.
한국천문연구원(KASI)·중국 농력과 동일한 규칙을 사용한다.
* 달의 시작 = 삭(New Moon)이 드는 날 (지방시 기준 자정~자정)
* 동지가 든 달 = 11월
* 동지에서 다음 동지까지 13삭망월이면 윤달이 있는 해이며,
중기(태양황경 30°의 배수)가 들지 않는 첫 번째 달이 윤달 (무중치윤법)
표준시: 기본 UTC+9 (한국). frame 인자 tz 또는 setTimezone()으로 변경 가능.
]]
local p = {}
local floor, sin, abs = math.floor, math.sin, math.abs
local RAD = math.pi / 180
local FIXED_TZ = nil
local MIN_YEAR, MAX_YEAR = 1600, 2200
-- 간지 ---------------------------------------------------------------------
local STEM = {'갑','을','병','정','무','기','경','신','임','계'}
local BRANCH = {'자','축','인','묘','진','사','오','미','신','유','술','해'}
local ZODIAC = {'쥐','소','호랑이','토끼','용','뱀','말','양','원숭이','닭','개','돼지'}
-- 유틸 ---------------------------------------------------------------------
local function mod360(x) return x - 360 * floor(x / 360) end
local function norm180(x) -- -180 ~ +180
x = mod360(x)
if x > 180 then x = x - 360 end
return x
end
local function gToJDN(y, m, d)
local a = floor((14 - m) / 12)
local yy = y + 4800 - a
local mm = m + 12 * a - 3
return d + floor((153 * mm + 2) / 5) + 365 * yy
+ floor(yy / 4) - floor(yy / 100) + floor(yy / 400) - 32045
end
local function jdnToG(jdn)
local a = jdn + 32044
local b = floor((4 * a + 3) / 146097)
local c = a - floor(146097 * b / 4)
local dd = floor((4 * c + 3) / 1461)
local e = c - floor(1461 * dd / 4)
local mm = floor((5 * e + 2) / 153)
local day = e - floor((153 * mm + 2) / 5) + 1
local month = mm + 3 - 12 * floor(mm / 10)
local year = 100 * b + dd - 4800 + floor(mm / 10)
return year, month, day
end
local function deltaT(year)
local t, u
if year < 1600 then
u = (year - 1820) / 100
return -20 + 32 * u * u
elseif year < 1700 then
t = year - 1600
return 120 - 0.9808 * t - 0.01532 * t ^ 2 + t ^ 3 / 7129
elseif year < 1800 then
t = year - 1700
return 8.83 + 0.1603 * t - 0.0059285 * t ^ 2 + 0.00013336 * t ^ 3 - t ^ 4 / 1174000
elseif year < 1860 then
t = year - 1800
return 13.72 - 0.332447 * t + 0.0068612 * t ^ 2 + 0.0041116 * t ^ 3
- 0.00037436 * t ^ 4 + 0.0000121272 * t ^ 5 - 0.0000001699 * t ^ 6
+ 0.000000000875 * t ^ 7
elseif year < 1900 then
t = year - 1860
return 7.62 + 0.5737 * t - 0.251754 * t ^ 2 + 0.01680668 * t ^ 3
- 0.0004473624 * t ^ 4 + t ^ 5 / 233174
elseif year < 1920 then
t = year - 1900
return -2.79 + 1.494119 * t - 0.0598939 * t ^ 2 + 0.0061966 * t ^ 3 - 0.000197 * t ^ 4
elseif year < 1941 then
t = year - 1920
return 21.20 + 0.84493 * t - 0.076100 * t ^ 2 + 0.0020936 * t ^ 3
elseif year < 1961 then
t = year - 1950
return 29.07 + 0.407 * t - t ^ 2 / 233 + t ^ 3 / 2547
elseif year < 1986 then
t = year - 1975
return 45.45 + 1.067 * t - t ^ 2 / 260 - t ^ 3 / 718
elseif year < 2005 then
t = year - 2000
return 63.86 + 0.3345 * t - 0.060374 * t ^ 2 + 0.0017275 * t ^ 3
+ 0.000651814 * t ^ 4 + 0.00002373599 * t ^ 5
elseif year < 2050 then
t = year - 2000
return 62.92 + 0.32217 * t + 0.005589 * t ^ 2
elseif year < 2150 then
return -20 + 32 * ((year - 1820) / 100) ^ 2 - 0.5628 * (2150 - year)
else
u = (year - 1820) / 100
return -20 + 32 * u * u
end
end
local function deltaTdays(jde)
local year = 2000 + (jde - 2451545) / 365.25
return deltaT(year) / 86400
end
local TZ_EPOCHS = {
{gToJDN(1912, 1, 1) - 0.5, 8 / 24}, -- ~1911
{gToJDN(1954, 3, 21) - 0.5, 9 / 24}, -- 1912~1954-03-20
{gToJDN(1961, 8, 10) - 0.5, 8.5 / 24}, -- 1954-03-21~1961-08-09
}
local function tzAt(jd)
if FIXED_TZ then return FIXED_TZ end
for i = 1, #TZ_EPOCHS do
if jd < TZ_EPOCHS[i][1] then return TZ_EPOCHS[i][2] end
end
return 9 / 24
end
local function jdeToLocalDay(jde)
local ut = jde - deltaTdays(jde)
return floor(ut + 0.5 + tzAt(ut))
end
local function sunLongitude(jde)
local T = (jde - 2451545) / 36525
local L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T * T
local M = 357.52911 + 35999.05029 * T - 0.0001537 * T * T
local Mr = M * RAD
local C = (1.914602 - 0.004817 * T - 0.000014 * T * T) * sin(Mr)
+ (0.019993 - 0.000101 * T) * sin(2 * Mr)
+ 0.000289 * sin(3 * Mr)
local theta = L0 + C
local omega = 125.04 - 1934.136 * T
return mod360(theta - 0.00569 - 0.00478 * sin(omega * RAD))
end
local function newMoonJDE(k)
local T = k / 1236.85
local T2, T3, T4 = T * T, T * T * T, T * T * T * T
local jde = 2451550.09766 + 29.530588861 * k
+ 0.00015437 * T2 - 0.000000150 * T3 + 0.00000000073 * T4
local E = 1 - 0.002516 * T - 0.0000074 * T2
local M = (2.5534 + 29.10535670 * k - 0.0000014 * T2 - 0.00000011 * T3) * RAD
local Mp = (201.5643 + 385.81693528 * k + 0.0107582 * T2
+ 0.00001238 * T3 - 0.000000058 * T4) * RAD
local F = (160.7108 + 390.67050284 * k - 0.0016118 * T2
- 0.00000227 * T3 + 0.000000011 * T4) * RAD
local Om = (124.7746 - 1.56375588 * k + 0.0020672 * T2 + 0.00000215 * T3) * RAD
jde = jde
- 0.40720 * sin(Mp)
+ 0.17241 * E * sin(M)
+ 0.01608 * sin(2 * Mp)
+ 0.01039 * sin(2 * F)
+ 0.00739 * E * sin(Mp - M)
- 0.00514 * E * sin(Mp + M)
+ 0.00208 * E * E * sin(2 * M)
- 0.00111 * sin(Mp - 2 * F)
- 0.00057 * sin(Mp + 2 * F)
+ 0.00056 * E * sin(2 * Mp + M)
- 0.00042 * sin(3 * Mp)
+ 0.00042 * E * sin(M + 2 * F)
+ 0.00038 * E * sin(M - 2 * F)
- 0.00024 * E * sin(2 * Mp - M)
- 0.00017 * sin(Om)
- 0.00007 * sin(Mp + 2 * M)
+ 0.00004 * sin(2 * Mp - 2 * F)
+ 0.00004 * sin(3 * M)
+ 0.00003 * sin(Mp + M - 2 * F)
+ 0.00003 * sin(2 * Mp + 2 * F)
- 0.00003 * sin(Mp + M + 2 * F)
+ 0.00003 * sin(Mp - M + 2 * F)
- 0.00002 * sin(Mp - M - 2 * F)
- 0.00002 * sin(3 * Mp + M)
+ 0.00002 * sin(4 * Mp)
local A = {
{299.77 + 0.107408 * k - 0.009173 * T2, 0.000325},
{251.88 + 0.016321 * k, 0.000165},
{251.83 + 26.651886 * k, 0.000164},
{349.42 + 36.412478 * k, 0.000126},
{ 84.66 + 18.206239 * k, 0.000110},
{141.74 + 53.303771 * k, 0.000062},
{207.14 + 2.453732 * k, 0.000060},
{154.84 + 7.306860 * k, 0.000056},
{ 34.52 + 27.261239 * k, 0.000047},
{207.19 + 0.121824 * k, 0.000042},
{291.34 + 1.844379 * k, 0.000040},
{161.72 + 24.198154 * k, 0.000037},
{239.56 + 25.513099 * k, 0.000035},
{331.55 + 3.592518 * k, 0.000023},
}
for i = 1, #A do
jde = jde + A[i][2] * sin(A[i][1] * RAD)
end
return jde
end
local function solarTermJDE(jde0, target)
local jde = jde0
for _ = 1, 8 do
local d = norm180(target - sunLongitude(jde))
if abs(d) < 1e-7 then break end
jde = jde + d * 365.2422 / 360
end
return jde
end
local function newMoonDay(k) return jdeToLocalDay(newMoonJDE(k)) end
local function kOnOrBefore(day)
local k = floor((day - 2451550.09766) / 29.530588861)
while newMoonDay(k) > day do k = k - 1 end
while newMoonDay(k + 1) <= day do k = k + 1 end
return k
end
local function winterSolsticeDay(year)
return jdeToLocalDay(solarTermJDE(gToJDN(year, 12, 21), 270))
end
local suiCache = {}
local function buildSui(year)
if suiCache[year] then return suiCache[year] end
local ws = winterSolsticeDay(year)
local k0 = kOnOrBefore(ws)
local k1 = kOnOrBefore(winterSolsticeDay(year + 1))
local n = k1 - k0 -- 12 또는 13
local starts = {}
for i = 0, n do starts[i] = newMoonDay(k0 + i) end
local leapIndex = -1
if n == 13 then
local terms = {}
local base = solarTermJDE(gToJDN(year, 12, 21), 270)
for j = 0, 13 do
terms[j] = jdeToLocalDay(solarTermJDE(base + 30.44 * j, mod360(270 + 30 * j)))
end
for i = 1, n - 1 do
local has = false
for j = 0, 13 do
if terms[j] >= starts[i] and terms[j] < starts[i + 1] then has = true break end
end
if not has then leapIndex = i break end
end
end
local months = {}
local num, ly = 11, year
for i = 0, n - 1 do
local m = {
start = starts[i],
days = starts[i + 1] - starts[i],
index = i,
}
if i == leapIndex then
m.num, m.leap, m.year = months[i - 1].num, true, months[i - 1].year
else
if num == 1 then ly = year + 1 end
m.num, m.leap, m.year = num, false, ly
num = num % 12 + 1
end
months[i] = m
end
local sui = {year = year, months = months, count = n,
first = starts[0], last = starts[n] - 1, nextStart = starts[n]}
suiCache[year] = sui
return sui
end
local function solarToLunar(y, m, d)
if y < MIN_YEAR or y > MAX_YEAR then return nil, '지원 범위(' .. MIN_YEAR .. '~' .. MAX_YEAR .. '년)를 벗어났습니다' end
local jdn = gToJDN(y, m, d)
local sui = buildSui(y)
if jdn < sui.first then sui = buildSui(y - 1) end
if jdn >= sui.nextStart then sui = buildSui(y + 1) end
for i = 0, sui.count - 1 do
local mo = sui.months[i]
if jdn >= mo.start and jdn < mo.start + mo.days then
return {
year = mo.year,
month = mo.num,
day = jdn - mo.start + 1,
leap = mo.leap,
days = mo.days,
jdn = jdn,
}
end
end
return nil, '변환 실패'
end
local function lunarToSolar(ly, lm, ld, leap)
if ly < MIN_YEAR or ly > MAX_YEAR then return nil, '지원 범위를 벗어났습니다' end
if lm < 1 or lm > 12 or ld < 1 or ld > 30 then return nil, '잘못된 음력 날짜입니다' end
local sui = buildSui(lm >= 11 and ly or ly - 1)
for i = 0, sui.count - 1 do
local mo = sui.months[i]
if mo.year == ly and mo.num == lm and (not mo.leap) == (not leap) then
if ld > mo.days then
return string.format('변환 실패')
end
local yy, mm, dd = jdnToG(mo.start + ld - 1)
return {year = yy, month = mm, day = dd, jdn = mo.start + ld - 1}
end
end
if leap then
return nil, string.format('음력 %d년에는 윤%d월이 없습니다', ly, lm)
end
return nil, '변환 실패'
end
local function ganzhi(idx)
idx = idx % 60
return STEM[idx % 10 + 1] .. BRANCH[idx % 12 + 1]
end
local function yearGanzhi(ly) return ganzhi((ly - 1984) % 60) end
local function dayGanzhi(jdn) return ganzhi((jdn + 49) % 60) end
local function zodiacOf(ly) return ZODIAC[(ly - 1984) % 12 + 1] end
p._solarToLunar = solarToLunar
p._lunarToSolar = lunarToSolar
p._buildSui = buildSui
p._ganzhi = { year = yearGanzhi, day = dayGanzhi, zodiac = zodiacOf }
p._toJDN = gToJDN
p._fromJDN = jdnToG
function p._setTimezone(hours)
FIXED_TZ = hours and (hours / 24) or nil
suiCache = {}
end
local function args(frame)
local a = {}
local parent = frame.getParent and frame:getParent()
if parent then for k, v in pairs(parent.args) do a[k] = v end end
for k, v in pairs(frame.args) do a[k] = v end
return a
end
local function num(v)
if v == nil then return nil end
v = tostring(v):gsub('^%s+', ''):gsub('%s+$', '')
return tonumber(v)
end
local function applyTZ(a)
local want = a.tz and num(a.tz) or nil
local cur = FIXED_TZ and FIXED_TZ * 24 or nil
if want ~= cur then p._setTimezone(want) end
end
local function err(msg)
return '<span class="error">' .. msg .. '</span>'
end
local function fmt(pattern, t, extra)
local map = {
['Y'] = t.year, ['n'] = t.month, ['j'] = t.day,
['m'] = t.month,
['d'] = t.day,
['L'] = extra and extra.leapMark or '',
}
return (pattern:gsub('%%(%a)', function(c) return tostring(map[c] or '%' .. c) end))
end
function p.toLunar(frame)
local a = args(frame)
local y, m, d = num(a[1]), num(a[2]), num(a[3])
if not (y and m and d) then return err('연·월·일을 입력하세요') end
applyTZ(a)
local r, e = solarToLunar(y, m, d)
if not r then return err(e) end
local leapMark = r.leap and (a.leaptext or '윤') or ''
if a.format then
return fmt(a.format, r, {leapMark = leapMark})
end
return string.format('%d년 %s%d월 %d일', r.year, leapMark, r.month, r.day)
end
function p.toSolar(frame)
local a = args(frame)
local y, m, d = num(a[1]), num(a[2]), num(a[3])
if not (y and m and d) then return err('연·월·일을 입력하세요') end
applyTZ(a)
local leap = a.leap and a.leap ~= '' and a.leap ~= '0' and a.leap ~= 'no'
local r, e = lunarToSolar(y, m, d, leap)
if not r then return err(e) end
if a.format then return fmt(a.format, r) end
return string.format('%d년 %d월 %d일', r.year, r.month, r.day)
end
function p.leapMonth(frame)
local a = args(frame)
local ly = num(a[1])
if not ly then return err('연도를 입력하세요') end
for _, y in ipairs({ly - 1, ly}) do
local sui = buildSui(y)
for i = 0, sui.count - 1 do
local mo = sui.months[i]
if mo.leap and mo.year == ly then return tostring(mo.num) end
end
end
return ''
end
function p.ganzhi(frame)
local a = args(frame)
local y, m, d = num(a[1]), num(a[2]), num(a[3])
if not y then return err('연도를 입력하세요') end
if m and d then
return dayGanzhi(gToJDN(y, m, d))
end
return yearGanzhi(y)
end
function p.zodiac(frame)
local y = num(args(frame)[1])
if not y then return err('연도를 입력하세요') end
return zodiacOf(y)
end
return p