lua/lunar.lua
--[[
	Lua 阿拉伯数字转中文实现 https://blog.csdn.net/lp12345678910/article/details/121396243
	农历功能复制自 https://github.com/boomker/rime-fast-xhup
--]]

-- 数字转中文：

local numerical_units = {
    "",
    "十",
    "百",
    "千",
    "万",
    "十",
    "百",
    "千",
    "亿",
    "十",
    "百",
    "千",
    "兆",
    "十",
    "百",
    "千",
}

local numerical_names = {
    "零",
    "一",
    "二",
    "三",
    "四",
    "五",
    "六",
    "七",
    "八",
    "九",
}

local function convert_arab_to_chinese(number)
    local n_number = tonumber(number)
    assert(n_number, "传入参数非正确number类型!")

    -- 0 ~ 9
    if n_number < 10 then
        return numerical_names[n_number + 1]
    end
    -- 一十九 => 十九
    if n_number < 20 then
        local digit = string.sub(n_number, 2, 2)
        if digit == "0" then
            return "十"
        else
            return "十" .. numerical_names[digit + 1]
        end
    end

    --[[
        1. 最大输入9位
            超过9位，string的len加2位（因为有.0的两位）
            零 ~ 九亿九千九百九十九万九千九百九十九
            0 ~ 999999999
        2. 最大输入14位（超过14位会四舍五入）
            零 ~ 九十九兆九千九百九十九亿九千九百九十九万九千九百九十九万
            0 ~ 99999999999999
    --]]
    local len_max = 9
    local len_number = string.len(number)
    assert(
        len_number > 0 and len_number <= len_max,
        "传入参数位数" .. len_number .. "必须在(0, " .. len_max .. "]之间！"
    )

    -- 01，数字转成表结构存储
    local numerical_tbl = {}
    for i = 1, len_number do
        numerical_tbl[i] = tonumber(string.sub(n_number, i, i))
    end

    local pre_zero = false
    local result = ""
    for index, digit in ipairs(numerical_tbl) do
        local curr_unit = numerical_units[len_number - index + 1]
        local curr_name = numerical_names[digit + 1]
        if digit == 0 then
            if not pre_zero then
                result = result .. curr_name
            end
            pre_zero = true
        else
            result = result .. curr_name .. curr_unit
            pre_zero = false
        end
    end
    result = string.gsub(result, "零+$", "")
    return result
end

-- 农历：

-- 天干名称
local cTianGan = {
    "甲",
    "乙",
    "丙",
    "丁",
    "戊",
    "己",
    "庚",
    "辛",
    "壬",
    "癸",
}

-- 地支名称
local cDiZhi = {
    "子",
    "丑",
    "寅",
    "卯",
    "辰",
    "巳",
    "午",
    "未",
    "申",
    "酉",
    "戌",
    "亥",
}

-- 属相名称
local cShuXiang = {
    "鼠",
    "牛",
    "虎",
    "兔",
    "龙",
    "蛇",
    "马",
    "羊",
    "猴",
    "鸡",
    "狗",
    "猪",
}

-- 农历日期名
local cDayName = {
    "初一",
    "初二",
    "初三",
    "初四",
    "初五",
    "初六",
    "初七",
    "初八",
    "初九",
    "初十",
    "十一",
    "十二",
    "十三",
    "十四",
    "十五",
    "十六",
    "十七",
    "十八",
    "十九",
    "二十",
    "廿一",
    "廿二",
    "廿三",
    "廿四",
    "廿五",
    "廿六",
    "廿七",
    "廿八",
    "廿九",
    "三十",
}

-- 农历月份名
local cMonName = {
    "正月",
    "二月",
    "三月",
    "四月",
    "五月",
    "六月",
    "七月",
    "八月",
    "九月",
    "十月",
    "冬月",
    "腊月",
}

-- 农历数据
local wNongliData = {
    "AB500D2",
    "4BD0883",
    "4AE00DB",
    "A5700D0",
    "54D0581",
    "D2600D8",
    "D9500CC",
    "655147D",
    "56A00D5",
    "9AD00CA",
    "55D027A",
    "4AE00D2",
    "A5B0682",
    "A4D00DA",
    "D2500CE",
    "D25157E",
    "B5500D6",
    "56A00CC",
    "ADA027B",
    "95B00D3",
    "49717C9",
    "49B00DC",
    "A4B00D0",
    "B4B0580",
    "6A500D8",
    "6D400CD",
    "AB5147C",
    "2B600D5",
    "95700CA",
    "52F027B",
    "49700D2",
    "6560682",
    "D4A00D9",
    "EA500CE",
    "6A9157E",
    "5AD00D6",
    "2B600CC",
    "86E137C",
    "92E00D3",
    "C8D1783",
    "C9500DB",
    "D4A00D0",
    "D8A167F",
    "B5500D7",
    "56A00CD",
    "A5B147D",
    "25D00D5",
    "92D00CA",
    "D2B027A",
    "A9500D2",
    "B550781",
    "6CA00D9",
    "B5500CE",
    "535157F",
    "4DA00D6",
    "A5B00CB",
    "457037C",
    "52B00D4",
    "A9A0883",
    "E9500DA",
    "6AA00D0",
    "AEA0680",
    "AB500D7",
    "4B600CD",
    "AAE047D",
    "A5700D5",
    "52600CA",
    "F260379",
    "D9500D1",
    "5B50782",
    "56A00D9",
    "96D00CE",
    "4DD057F",
    "4AD00D7",
    "A4D00CB",
    "D4D047B",
    "D2500D3",
    "D550883",
    "B5400DA",
    "B6A00CF",
    "95A1680",
    "95B00D8",
    "49B00CD",
    "A97047D",
    "A4B00D5",
    "B270ACA",
    "6A500DC",
    "6D400D1",
    "AF40681",
    "AB600D9",
    "93700CE",
    "4AF057F",
    "49700D7",
    "64B00CC",
    "74A037B",
    "EA500D2",
    "6B50883",
    "5AC00DB",
    "AB600CF",
    "96D0580",
    "92E00D8",
    "C9600CD",
    "D95047C",
    "D4A00D4",
    "DA500C9",
    "755027A",
    "56A00D1",
    "ABB0781",
    "25D00DA",
    "92D00CF",
    "CAB057E",
    "A9500D6",
    "B4A00CB",
    "BAA047B",
    "AD500D2",
    "55D0983",
    "4BA00DB",
    "A5B00D0",
    "5171680",
    "52B00D8",
    "A9300CD",
    "795047D",
    "6AA00D4",
    "AD500C9",
    "5B5027A",
    "4B600D2",
    "96E0681",
    "A4E00D9",
    "D2600CE",
    "EA6057E",
    "D5300D5",
    "5AA00CB",
    "76A037B",
    "96D00D3",
    "4AB0B83",
    "4AD00DB",
    "A4D00D0",
    "D0B1680",
    "D2500D7",
    "D5200CC",
    "DD4057C",
    "B5A00D4",
    "56D00C9",
    "55B027A",
    "49B00D2",
    "A570782",
    "A4B00D9",
    "AA500CE",
    "B25157E",
    "6D200D6",
    "ADA00CA",
    "4B6137B",
    "93700D3",
    "49F08C9",
    "49700DB",
    "64B00D0",
    "68A1680",
    "EA500D7",
    "6AA00CC",
    "A6C147C",
    "AAE00D4",
    "92E00CA",
    "D2E0379",
    "C9600D1",
    "D550781",
    "D4A00D9",
    "DA400CD",
    "5D5057E",
    "56A00D6",
    "A6C00CB",
    "55D047B",
    "52D00D3",
    "A9B0883",
    "A9500DB",
    "B4A00CF",
    "B6A067F",
    "AD500D7",
    "55A00CD",
    "ABA047C",
    "A5A00D4",
    "52B00CA",
    "B27037A",
    "69300D1",
    "7330781",
    "6AA00D9",
    "AD500CE",
    "4B5157E",
    "4B600D6",
    "A5700CB",
    "54E047C",
    "D1600D2",
    "E960882",
    "D5200DA",
    "DAA00CF",
    "6AA167F",
    "56D00D7",
    "4AE00CD",
    "A9D047D",
    "A2D00D4",
    "D1500C9",
    "F250279",
    "D5200D1",
}

-- 十进制转二进制
local function Dec2bin(n)
    local t, t1
    local tables = {}
    t = tonumber(n)
    while math.floor(t / 2) >= 1 do
        t1 = t and math.fmod(t, 2)
        if t1 > 0 then
            if #tables > 0 then
                table.insert(tables, 1, 1)
            else
                tables[1] = 1
            end
        else
            if #tables > 0 then
                table.insert(tables, 1, 0)
            else
                tables[1] = 0
            end
        end
        t = math.floor(t / 2)
        if t == 1 then
            if #tables > 0 then
                table.insert(tables, 1, 1)
            else
                tables[1] = 1
            end
        end
    end
    return string.gsub(table.concat(tables), "^[0]+", "")
end

-- 2/10/16进制互转
local function Atoi(x, inPuttype, outputtype)
    local r
    if tonumber(inPuttype) == 2 then
        if tonumber(outputtype) == 10 then -- 2进制-->10进制
            r = tonumber(tostring(x), 2)
            -- elseif tonumber(outputtype) == 16 then -- 2进制-->16进制
            -- 	r = bin2hex(tostring(x))
        end
    elseif tonumber(inPuttype) == 10 then
        if tonumber(outputtype) == 2 then -- 10进制-->2进制
            r = Dec2bin(tonumber(x))
        elseif tonumber(outputtype) == 16 then -- 10进制-->16进制
            r = string.format("%x", x)
        end
    elseif tonumber(inPuttype) == 16 then
        if tonumber(outputtype) == 2 then -- 16进制-->2进制
            r = Dec2bin(tonumber(tostring(x), 16))
        elseif tonumber(outputtype) == 10 then -- 16进制-->10进制
            r = tonumber(tostring(x), 16)
        end
    end
    return r
end

-- 农历16进制数据分解
local function Analyze(Data)
    local rtn1, rtn2, rtn3, rtn4
    rtn1 = Atoi(string.sub(Data, 1, 3), 16, 2)
    if string.len(rtn1) < 12 then
        rtn1 = "0" .. rtn1
    end
    rtn2 = string.sub(Data, 4, 4)
    rtn3 = Atoi(string.sub(Data, 5, 5), 16, 10)
    rtn4 = Atoi(string.sub(Data, -2, -1), 16, 10)
    if string.len(rtn4) == 3 then
        rtn4 = "0" .. Atoi(string.sub(Data, -2, -1), 16, 10)
    end
    -- string.gsub(rtn1, "^[0]*", "")
    return { rtn1, rtn2, rtn3, rtn4 }
end

-- 年天数判断
local function IsLeap(y)
    local year = tonumber(y)
    if not year then
        return nil
    end
    if math.fmod(year, 400) ~= 0 and math.fmod(year, 4) == 0 or math.fmod(year, 400) == 0 then
        return 366
    else
        return 365
    end
end

-- 返回当年过了多少天
local function leaveDate(y)
    local day, total
    total = 0
    if IsLeap(tonumber(string.sub(y, 1, 4))) > 365 then
        day = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
    else
        day = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
    end
    if tonumber(string.sub(y, 5, 6)) > 1 then
        for i = 1, tonumber(string.sub(y, 5, 6)) - 1 do
            total = total + day[i]
        end
        total = total + tonumber(string.sub(y, 7, 8))
    else
        return tonumber(string.sub(y, 7, 8))
    end
    return tonumber(total)
end

-- 计算日期差，两个8位数日期之间相隔的天数，date2>date1
local function diffDate(date1, date2)
    local n, total
    total = 0
    date1 = tostring(date1)
    date2 = tostring(date2)
    if tonumber(date2) > tonumber(date1) then
        n = tonumber(string.sub(date2, 1, 4)) - tonumber(string.sub(date1, 1, 4))
        if n > 1 then
            for i = 1, n - 1 do
                total = total + IsLeap(tonumber(string.sub(date1, 1, 4)) + i)
            end
            total = total
                + leaveDate(tonumber(string.sub(date2, 1, 8)))
                + IsLeap(tonumber(string.sub(date1, 1, 4)))
                - leaveDate(tonumber(string.sub(date1, 1, 8)))
        elseif n == 1 then
            total = IsLeap(tonumber(string.sub(date1, 1, 4)))
                - leaveDate(tonumber(string.sub(date1, 1, 8)))
                + leaveDate(tonumber(string.sub(date2, 1, 8)))
        else
            total = leaveDate(tonumber(string.sub(date2, 1, 8))) - leaveDate(tonumber(string.sub(date1, 1, 8)))
            -- print(date1 .. "-" .. date2)
        end
    elseif tonumber(date2) == tonumber(date1) then
        return 0
    else
        return -1
    end
    return total
end

-- 公历转农历，支持转化范围公元1900-2100年
-- 公历日期 Gregorian:格式 YYYYMMDD
-- <返回值>农历日期 中文 天干地支属相
local function Date2LunarDate(Gregorian)
    Gregorian = tostring(Gregorian)
    local Year, Month, Day, Pos, Data0, Data1, MonthInfo, LeapInfo, Leap, Newyear, LYear, thisMonthInfo
    Year = tonumber(Gregorian.sub(Gregorian, 1, 4))
    Month = tonumber(Gregorian.sub(Gregorian, 5, 6))
    Day = tonumber(Gregorian.sub(Gregorian, 7, 8))
    if Year > 2100 or Year < 1899 or Month > 12 or Month < 1 or Day < 1 or Day > 31 or string.len(Gregorian) < 8 then
        return "无效日期", "无效日期"
    end

    -- 获取两百年内的农历数据
    Pos = Year - 1900 + 2
    Data0 = wNongliData[Pos - 1]
    Data1 = wNongliData[Pos]
    -- 判断农历年份
    local tb1 = Analyze(Data1)
    MonthInfo = tb1[1]
    LeapInfo = tb1[2]
    Leap = tb1[3]
    Newyear = tb1[4]
    local Date1 = Year .. Newyear
    local Date2 = Gregorian
    local Date3 = diffDate(Date1, Date2) -- 和当年农历新年相差的天数
    if Date3 < 0 then
        -- print(Data0 .. "-2")
        tb1 = Analyze(Data0)
        Year = Year - 1
        MonthInfo = tb1[1]
        LeapInfo = tb1[2]
        Leap = tb1[3]
        Newyear = tb1[4]
        Date1 = Year .. Newyear
        Date2 = Gregorian
        Date3 = diffDate(Date1, Date2)
        -- print(Date2 .. "--" .. Date1 .. "--" .. Date3)
    end

    Date3 = Date3 + 1
    LYear = Year  -- 农历年份，就是上面计算后的值
    if Leap > 0 then -- 有闰月
        thisMonthInfo = string.sub(MonthInfo, 1, tonumber(Leap)) .. LeapInfo .. string.sub(MonthInfo, Leap + 1)
    else
        thisMonthInfo = MonthInfo
    end

    local thisMonth, thisDays, LMonth, LDay, Isleap, LunarDate, LunarDate2, LunarYear, LunarMonth
    for i = 1, 13 do
        thisMonth = string.sub(thisMonthInfo, i, i)
        thisDays = 29 + thisMonth
        if Date3 > thisDays then
            Date3 = Date3 - thisDays
        else
            if Leap > 0 then
                if Leap >= i then
                    LMonth = i
                    Isleap = 0
                else
                    LMonth = i - 1
                    if i - Leap == 1 then
                        Isleap = 1
                    else
                        Isleap = 0
                    end
                end
            else
                LMonth = i
                Isleap = 0
            end
            LDay = math.floor(Date3)
            break
        end
    end

    if Isleap > 0 then
        LunarMonth = "闰" .. cMonName[LMonth]
    else
        LunarMonth = cMonName[LMonth]
    end

    local _nis = tostring(LYear)
    local _LunarYears = ""
    for i = 1, _nis:len() do
        local _ni_digit = tonumber(_nis:sub(i, i))
        _LunarYears = _LunarYears .. convert_arab_to_chinese(_ni_digit)
    end

    LunarYear = string.gsub(_LunarYears, "零", "〇")
    LunarDate = cTianGan[math.fmod(LYear - 4, 10) + 1]
        .. cDiZhi[math.fmod(LYear - 4, 12) + 1]
        .. "年（"
        .. cShuXiang[math.fmod(LYear - 4, 12) + 1]
        .. "）"
        .. LunarMonth
        .. cDayName[LDay]

    LunarDate2 = LunarYear .. "年" .. LunarMonth .. cDayName[LDay]
    return LunarDate, LunarDate2
end

-- 农历
-- 从 lunar: nl 获取农历触发关键字（双拼默认为 lunar）
-- 从 recognizer/patterns/gregorian_to_lunar 获取第 2 个字符作为公历转农历的触发前缀，默认为 N
local function translator(input, seg, env)
    env.lunar_key_word = env.lunar_key_word or
        (env.engine.schema.config:get_string(env.name_space:gsub('^*', '')) or 'nl')
    env.gregorian_to_lunar = env.gregorian_to_lunar or
        (env.engine.schema.config:get_string('recognizer/patterns/gregorian_to_lunar'):sub(2, 2) or 'N')
    if input == env.lunar_key_word then
        local date1, date2 = Date2LunarDate(os.date("%Y%m%d"))
        local lunar_ymd = (Candidate("", seg.start, seg._end, date2, ""))
        lunar_ymd.quality = 999
        yield(lunar_ymd)
        local lunar_date = Candidate("", seg.start, seg._end, date1, "")
        lunar_date.quality = 999
        yield(lunar_date)
    elseif env.gregorian_to_lunar ~= '' and input:sub(1, 1) == env.gregorian_to_lunar then
        local date1, date2 = Date2LunarDate(input:sub(2))
        local lunar_ymd = (Candidate("", seg.start, seg._end, date2, ""))
        lunar_ymd.quality = 999
        yield(lunar_ymd)
        local lunar_date = Candidate("", seg.start, seg._end, date1, "")
        lunar_date.quality = 999
        yield(lunar_date)
    end
end

return translator
