42

阴历阳历的相互转换(支持1900~2100年)

 5 years ago
source link: https://studygolang.com/articles/14740?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

背景

最近做到一个项目, 需要阴历与阳历的相互转换, 网上找了很多资料, 发现很多都是不准的, 但是给了我参考价值

算法

借用百度百科的 :

阳历

太阳历又称为阳历,是以地球绕太阳公转的运动周期为基础而制定的历法。

太阳历的历年近似等于回归年,一年12个月,这个“月”,实际上与朔望月无关。

阳历的月份、日期都与太阳在黄道上的位置较好地符合,根据阳历的日期,在一年中可以明显看出四季寒暖变化的情况;但在每个月份中,看不出月亮的朔、望、两弦。

如今世界通行的公历就是一种阳历,平年365天,闰年366天,每四年一闰,每满百年少闰一次,到第四百年再闰,即每四百年中有97个闰年。公历的历年平均长度与回归年只有26秒之差,要累积3300年才差一日。

阴历

希吉来历系太阴历,其计算方法是: 以太阴圆缺一周为一月,历时29日12小时44分2.8秒,太阴圆缺十二周为一年,历时354日8小时48分33.6秒。每一年的12个月中,6个单数月份(即1、3、5、7、9、11月)为“大建”,每月为30天; 6个双数月份(2、4、6、8、10、12月)为“小建”,每月为29天;在逢闰之年,将12月改大月为30天。该历以30年为一周期,每一周期里的第2、5、7、10、13、16、18、21、24、26、29年,共11年为闰年, 不设置闰月,而在12月末置一闰日,闰年为355日,另19年为平年,每年354日。故平均每年为354日8小时48分。按该历全年实际天数计算,比回归年约少10日21小时1分,积2.7回归年相差一月,积32.6回归年相差一年。该历对昼夜的计算,以日落为一天之始,到次日日落为一日,通常称为夜行前,即黑夜在前,白昼在后,构成一天。希吉来历每年9月(莱麦丹)为伊斯兰教斋戒之月, 对这个月的起讫除了计算之外,还要由观察新月是否出现来决定。 即在8月29日这天进行观测,如见新月,第二日即为9月1日,黎明前开始斋戒,8月仍为小建; 如不见新月,第三日则为9月1日,8月即变为“大建”。到了9月29日傍晚,也需要看月,如见新月,第二天就是10月1日,即为开斋节日,使9月变成“小建”;如未见新月,斋戒必须再延一天,9月即为“大建”。 12月(祖勒·希哲)上旬为朝觐日期,12月10日为宰牲节日。该历的星期,使用七曜(日、月、火、水、木、金、土)记日的周日法。每周逢金曜为“主麻日”,穆斯林在这一天举行“聚礼”。

思路

借用: https://blog.csdn.net/hsd2012... 这里的解释: 要想计算给定的时间对于的农历是哪一天,我们需要找一个参考时间,然后以该参考时间计算以后的时间。首先计算当前时间与参考时间相差的天数,然后通过求出农历每年的天数,计算当前时间对应的是哪一年的第几天,最后计算出属于那个月的哪一个日期。

计算生肖属相

目前的做法是 阴历年份 - 1900 + 36 然后除以 12 取余数, 得出生肖属相的序号, 至于为啥这么计算, 我是没搞懂, 网上也搜了很多也没说清楚, 这里参考文章: https://juejin.im/entry/59904...

private function yearShengXiao($lunarYear)
{
    // TODO 至于为什么这样弄, 我也没搞清楚
    return self::SHENG_XIAO[($lunarYear - self::MIN_YEAR + 36) % 12]; // 年的属相
}

年的干支算法

网上搜到的做法是用公元年来计算, 但是不对, 然后我换成阴历年居然就跟百度的日历能对上了, 这个我也没弄清楚, 但是能算出来了, 公式: 年数先减三,除10余数是天干,基数改用12除,余数便是地支年 (如果余数为 0 ,则取最大序号)

private function yearGanZhi($lunarYear)
{
    // 年数先减三,除10余数是天干,基数改用12除,余数便是地支年 (如果余数为 0 ,则取最大序号)
    $yJiShu = $lunarYear - 3;
    $yTianGan = ($yJiShu % 10 == 0) ? 10 : $yJiShu % 10;
    $yDiZhi = ($yJiShu % 12 == 0) ? 10 : $yJiShu % 12;
    $yGanZhi = self::TIAN_GAN[$yTianGan - 1] . self::DI_ZHI[$yDiZhi - 1]; // // 由于是从 0 开始,这里再减一

    return $yGanZhi;
}

月的干支算法

网上搜索了, 没找到好的实现方式, 麻烦知道的在这里说一下,

日的干支算法

网上搜到的:

G = 4C + [C / 4] + 5y + [y / 4] + [3 * (M + 1) / 5] + d - 3

Z = 8C + [C / 4] + 5y + [y / 4] + [3 * (M + 1) / 5] + d + 7 + i

其中C 是世纪数减一,y 是年份后两位,M 是月份,d 是日数。1月和2月按上一年的13月和14月来算。奇数月i=0,偶数月i=6。G 除以10的余数是天干,Z 除以12的余数是地支。

但是不对, 麻烦有懂也告知下

PHP 的实现完整代码

<?php

/**
 * 阳历和新历的转换
 */
class SolarLunar {

    // 最小年
    const MIN_YEAR = 1900;
    // 最大年
    const MAX_YEAR = 2100;

    // 开始的日期
    const START_DATE_STR = "1900-01-30";


    const CHINESE_NUMBER = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"];
    const CHINESE_NUMBER_SPECIAL = ["正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊"];

    const TIAN_GAN = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
    const DI_ZHI = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
    const SHENG_XIAO = ["鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"];

    // 阴历年份的数据
    const LUNAR_INFO = [
        0x04bd8,0x04ae0,0x0a570,0x054d5,0x0d260,0x0d950,0x16554,0x056a0,0x09ad0,0x055d2,//1900-1909
        0x04ae0,0x0a5b6,0x0a4d0,0x0d250,0x1d255,0x0b540,0x0d6a0,0x0ada2,0x095b0,0x14977,//1910-1919
        0x04970,0x0a4b0,0x0b4b5,0x06a50,0x06d40,0x1ab54,0x02b60,0x09570,0x052f2,0x04970,//1920-1929
        0x06566,0x0d4a0,0x0ea50,0x06e95,0x05ad0,0x02b60,0x186e3,0x092e0,0x1c8d7,0x0c950,//1930-1939
        0x0d4a0,0x1d8a6,0x0b550,0x056a0,0x1a5b4,0x025d0,0x092d0,0x0d2b2,0x0a950,0x0b557,//1940-1949
        0x06ca0,0x0b550,0x15355,0x04da0,0x0a5b0,0x14573,0x052b0,0x0a9a8,0x0e950,0x06aa0,//1950-1959
        0x0aea6,0x0ab50,0x04b60,0x0aae4,0x0a570,0x05260,0x0f263,0x0d950,0x05b57,0x056a0,//1960-1969
        0x096d0,0x04dd5,0x04ad0,0x0a4d0,0x0d4d4,0x0d250,0x0d558,0x0b540,0x0b6a0,0x195a6,//1970-1979
        0x095b0,0x049b0,0x0a974,0x0a4b0,0x0b27a,0x06a50,0x06d40,0x0af46,0x0ab60,0x09570,//1980-1989
        0x04af5,0x04970,0x064b0,0x074a3,0x0ea50,0x06b58,0x055c0,0x0ab60,0x096d5,0x092e0,//1990-1999
        0x0c960,0x0d954,0x0d4a0,0x0da50,0x07552,0x056a0,0x0abb7,0x025d0,0x092d0,0x0cab5,//2000-2009
        0x0a950,0x0b4a0,0x0baa4,0x0ad50,0x055d9,0x04ba0,0x0a5b0,0x15176,0x052b0,0x0a930,//2010-2019
        0x07954,0x06aa0,0x0ad50,0x05b52,0x04b60,0x0a6e6,0x0a4e0,0x0d260,0x0ea65,0x0d530,//2020-2029
        0x05aa0,0x076a3,0x096d0,0x04bd7,0x04ad0,0x0a4d0,0x1d0b6,0x0d250,0x0d520,0x0dd45,//2030-2039
        0x0b5a0,0x056d0,0x055b2,0x049b0,0x0a577,0x0a4b0,0x0aa50,0x1b255,0x06d20,0x0ada0,//2040-2049
        0x14b63,0x09370,0x049f8,0x04970,0x064b0,0x168a6,0x0ea50, 0x06b20,0x1a6c4,0x0aae0,//2050-2059
        0x0a2e0,0x0d2e3,0x0c960,0x0d557,0x0d4a0,0x0da50,0x05d55,0x056a0,0x0a6d0,0x055d4,//2060-2069
        0x052d0,0x0a9b8,0x0a950,0x0b4a0,0x0b6a6,0x0ad50,0x055a0,0x0aba4,0x0a5b0,0x052b0,//2070-2079
        0x0b273,0x06930,0x07337,0x06aa0,0x0ad50,0x14b55,0x04b60,0x0a570,0x054e4,0x0d160,//2080-2089
        0x0e968,0x0d520,0x0daa0,0x16aa6,0x056d0,0x04ae0,0x0a9d4,0x0a2d0,0x0d150,0x0f252,//2090-2099
        0x0d520];


    /**
     * 阴历转阳历
     * @param string $date
     * @param bool $leapMonthFlag
     * @return string
     */
    public function lunarToSolar($date = "", $leapMonthFlag = false) {
        try {
            $dateTime = new \DateTime($date);
        } catch (\Exception $exception) {
            return "";
        }
        $lunarYear = $dateTime->format("Y");
        $lunarMonth = $dateTime->format("n");
        $lunarDay = $dateTime->format("j");
        // 检查是否合法
        if (!$this->checkLunarDate($lunarYear, $lunarMonth, $lunarDay, $leapMonthFlag)) {
            return "";
        }

        $offset = 0;
        for ($i = self::MIN_YEAR; $i < $lunarYear; $i++) {
            $yearDaysCount = $this->getYearDays($i); // 求阴历某年天数
            $offset += $yearDaysCount;
        }
        //计算该年闰几月
        $leapMonth = $this->getLeapMonth($lunarYear);
        if ($leapMonthFlag && $leapMonth != $lunarMonth) {
            // 您输入的闰月标志有误
            return "";
        }

        if ($leapMonth == 0 || ($lunarMonth < $leapMonth) || ($lunarMonth == $leapMonth && !$leapMonthFlag)) {
            for ($i = 1; $i < $lunarMonth; $i++) {
                $tempMonthDaysCount = $this->getMonthDays($lunarYear, $i);
                $offset += $tempMonthDaysCount;
            }

            // 检查日期是否大于最大天
            if ($lunarDay > $this->getMonthDays($lunarYear, $lunarMonth)) {
                // 不合法的农历日期
                return "";
            }
            $offset += intval($lunarDay); // 加上当月的天数
        } else { //当年有闰月,且月份晚于或等于闰月
            for ($i = 1; $i < $lunarMonth; $i++) {
                $tempMonthDaysCount = $this->getMonthDays($lunarYear, $i);
                $offset += $tempMonthDaysCount;
            }
            if ($lunarMonth > $leapMonth) {
                $temp = $this->getLeapMonthDays($lunarYear); // 计算闰月天数
                $offset += $temp;                      // 加上闰月天数

                if ($lunarDay > $this->getMonthDays($lunarYear, $lunarMonth)) {
                    // 不合法的农历日期
                    return "";
                }
                $offset += intval($lunarDay);
            } else { // 如果需要计算的是闰月,则应首先加上与闰月对应的普通月的天数
                // 计算月为闰月
                $temp = $this->getMonthDays($lunarYear, $lunarMonth); // 计算非闰月天数
                $offset += $temp;

                if ($lunarDay > $this->getLeapMonthDays($lunarYear)) {
                    // 不合法的农历日期
                    return "";
                }
                $offset += intval($lunarDay);
            }
        }

        try {
            $newDateTime = new \DateTime(self::START_DATE_STR);
        } catch (\Exception $exception) {
            return "";
        }

        $sumH = 24 * $offset;
        $newDateTime->add(new \DateInterval('PT' . $sumH . 'H'));

        return $newDateTime->format("Y-m-d");
    }


    /**
     * 把阳历转换为中文的阴历
     * @param string $date
     * @return string
     */
    public function solarToChineseLunar($date)
    {
        $dateTime = new \DateTime($date);
        list($lunarYear, $lunarMonth, $lunarDay, $leapMonth, $leapMonthFlag) = $this->calculateLunar($dateTime);
        $result = "";
        if($leapMonthFlag && $lunarMonth == $leapMonth){
            $result .= "闰";
        }
        $solarYear = (int)$dateTime->format("Y");
        $solarMoth = (int)$dateTime->format("n");
        $solarDay = (int)$dateTime->format("j");
        $result .= self::CHINESE_NUMBER_SPECIAL[$lunarMonth-1] . "月";
        $result .= $this->chineseDayString($lunarDay) . "日";
        // 年的天干地支
        $yGanZhi = $this->yearGanZhi($lunarYear);
        // 生肖属相
        $yShengXiao = $this->yearShengXiao($lunarYear);
        $result .= "," . $yGanZhi . "年 [" . $yShengXiao . '年]';
        // 月的天干地支
        $mTianGanDiZhi = $this->mothGanZhi($solarYear, $solarMoth, $solarDay);
        $result .= "," . $mTianGanDiZhi . '月';
        // 日的天干地支
        $dTianGanDiZhi = $this->dayGanZhi($solarYear, $solarMoth, $solarDay);
        $result .= "," . $dTianGanDiZhi . '日';
        return $result;
    }

    /**
     * 阳历转换为简单的中文阴历
     * @param string $date
     * @return string
     */
    public function solarToSimpleLunar($date)
    {
        $dateTime = new \DateTime($date);
        list($lunarYear, $lunarMonth, $lunarDay, $leapMonth, $leapMonthFlag) = $this->calculateLunar($dateTime);
        $result = $lunarYear . "年";
        if($leapMonthFlag && $lunarMonth == $leapMonth) {
            $result .= "闰";
        }
        if($lunarMonth < 10){
            $result .= "0" . $lunarMonth . "月";
        } else {
            $result .= $lunarMonth . "月";
        }
        if($lunarDay < 10){
            $result .= "0" . $lunarDay . "日";
        } else {
            $result .= $lunarDay . "日";
        }
        return $result;
    }

    /**
     * 阳历转为正常的阴历
     * @param string $date
     * @param bool $isLeapMonth 是否是闰月
     * @return string
     */
    public function solarToLunar($date, &$isLeapMonth = false)
    {
        $dateTime = new \DateTime($date);
        list($lunarYear, $lunarMonth, $lunarDay, $leapMonth, $leapMonthFlag) = $this->calculateLunar($dateTime);
        $result = $lunarYear . "-";
        if($lunarMonth < 10){
            $result .= "0" . $lunarMonth . "-";
        } else {
            $result .= $lunarMonth . "-";
        }
        if($lunarDay < 10){
            $result .= "0" . $lunarDay;
        } else {
            $result .= $lunarDay;
        }
        $isLeapMonth = false;
        if($leapMonthFlag && $lunarMonth == $leapMonth) {
            $isLeapMonth = true;
        }
        return $result;
    }

    /**
     * 计算当前日期的阴历
     * @param \DateTime $dateTime
     * @return array
     */
    private function calculateLunar($dateTime)
    {
        $i = 0;
        $temp = 0;
        $leapMonthFlag = false;
        $isLeapYear = false;

        $startDate = new \DateTime(self::START_DATE_STR);
        $offset = $this->daysBwteen($dateTime, $startDate);
        for($i = self::MIN_YEAR; $i < self::MAX_YEAR; $i++){
            $temp = $this->getYearDays($i); //求当年农历年天数
            if($offset - $temp < 1){
                break;
            } else {
                $offset -= $temp;
            }
        }
        $lunarYear = $i;

        $leapMonth = $this->getLeapMonth($lunarYear); //计算该年闰哪个月

        //设定当年是否有闰月
        if($leapMonth > 0 ){
            $isLeapYear = true;
        } else {
            $isLeapYear = false;
        }

        for($i = 1; $i <= 12; $i++){
            if($i == $leapMonth + 1 && $isLeapYear){
                $temp = $this->getLeapMonthDays($lunarYear);
                $isLeapYear = false;
                $leapMonthFlag = true;
                $i--;
            } else {
                $temp = $this->getMonthDays($lunarYear, $i);
            }
            $offset -= $temp;
            if($offset <= 0){
                break;
            }
        }
        $offset += $temp;
        $lunarMonth = $i;
        $lunarDay = $offset;
        return [$lunarYear, $lunarMonth, $lunarDay, $leapMonth, $leapMonthFlag];
    }

    /**
     * 检查阴历是否合法
     * @param int $lunarYear
     * @param int $lunarMonth
     * @param int $lunarDay
     * @param bool $leapMonthFlag
     * @return bool
     * @throws Exception
     */
    private function checkLunarDate($lunarYear, $lunarMonth, $lunarDay, $leapMonthFlag = false) {
        if ($lunarYear < self::MIN_YEAR || $lunarYear > self::MAX_YEAR) {
            // 非法农历年份
            return false;
        }
        if ($lunarMonth < 1 || $lunarMonth > 12) {
            // 非法农历月份
            return false;
        }
        if ($lunarDay < 1 || $lunarDay > 30) { // 中国的月最多30天
            // 非法农历天数
            return false;
        }
        $leap = $this->getLeapMonth($lunarYear); // 计算该年应该闰哪个月
        if ($leapMonthFlag == true && $lunarMonth != $leap) {
            // 非法闰月
            return false;
        }
        return true;
    }

    /**
     * 计算该月总天数
     * @param int $year
     * @param int $month
     * @return int
     */
    private function getMonthDays($year, $month) {
        if ($month > 31 || $month < 0) {
            // error month
            return 0;
        }
        // 0X0FFFF[0000 {1111 1111 1111} 1111]中间12位代表12个月,1为大月,0为小月
        $bit = 1 << (16 - $month);
        if (((self::LUNAR_INFO[$year - 1900] & 0x0FFFF) & $bit) == 0) {
            return 29;
        }
        return 30;
    }

    /**
     * 计算阴历年的总天数
     * @param int $year
     * @return int
     */
    private function getYearDays($year) {
        $sum = 29 * 12;
        for ($i = 0x8000; $i >= 0x8; $i >>= 1) {
            if ((self::LUNAR_INFO[$year - 1900] & 0xfff0 & $i) != 0) {
                $sum++;
            }
        }
        return $sum + $this->getLeapMonthDays($year);
    }

    /**
     * 计算阴历年闰月多少天
     * @param int $year
     * @return int
     */
    private function getLeapMonthDays($year) {
        if ($this->getLeapMonth($year) != 0) {
            if ((self::LUNAR_INFO[$year - 1900] & 0xf0000) == 0) {
                return 29;
            }
            return 30;
        }
        return 0;
    }

    /**
     * 计算阴历年闰哪个月 1-12 , 没闰传回 0
     * @param int $year
     * @return int
     */
    private function getLeapMonth($year) {
        return (int)(self::LUNAR_INFO[$year - 1900] & 0xf);
    }

    /**
     * 计算差的天数
     * @param \DateTime $date
     * @param \DateTime $startDate
     * @return int
     */
    private function daysBwteen($date, $startDate)
    {
        $subValue = floatval($date->getTimestamp() - $startDate->getTimestamp()) / 86400.0 + 0.5;
        return intval($subValue);
    }

    /**
     * 计算年天干地支
     * @param $lunarYear
     * @return string
     */
    private function yearGanZhi($lunarYear)
    {
        // 年数先减三,除10余数是天干,基数改用12除,余数便是地支年 (如果余数为 0 ,则取最大序号)
        $yJiShu = $lunarYear - 3;
        $yTianGan = ($yJiShu % 10 == 0) ? 10 : $yJiShu % 10;
        $yDiZhi = ($yJiShu % 12 == 0) ? 10 : $yJiShu % 12;
        $yGanZhi = self::TIAN_GAN[$yTianGan - 1] . self::DI_ZHI[$yDiZhi - 1]; // // 由于是从 0 开始,这里再减一

        return $yGanZhi;
    }

    /**
     * 计算年的生肖属相
     * @param $lunarYear
     * @return mixed
     */
    private function yearShengXiao($lunarYear)
    {
        // TODO 至于为什么这样弄, 我也没搞清楚
        return self::SHENG_XIAO[($lunarYear - self::MIN_YEAR + 36) % 12]; // 年的属相
    }

    // TODO 尚未实现
    /**
     * 计算日的天干地支
     * @param $solarYear
     * @param $solarDay
     * @param $solarDay
     * @return string
     */
    private function dayGanZhi($solarYear, $solarDay, $solarDay)
    {
        return "";
    }

    // TODO 尚未实现
    /**
     * 计算月的天干地支
     * @param $solarYear
     * @param $solarDay
     * @param $solarDay
     * @return string
     */
    private function mothGanZhi($solarYear, $solarDay, $solarDay)
    {
        return "";
    }

    /**
     * 把天转换为中文字符
     * @param int $day
     * @return mixed|string
     */
    private function chineseDayString($day)
    {
        $chineseTen = ["初", "十", "廿", "三"];
        $n = 0;
        if($day % 10 == 0){
            $n = 9;
        } else {
            $n = $day % 10 - 1;
        }
        if($day > 30){
            return "";
        }
        if($day == 20){
            return "二十";
        } else if($day == 10){
            return "初十";
        } else {
            return $chineseTen[$day / 10] . self::CHINESE_NUMBER[$n];
        }
    }

}


$solarLunar = new SolarLunar();

$solarDate = "2010-01-05";
$new_date = $solarLunar->solarToChineseLunar($solarDate);
var_dump($solarDate . " 转为阴历: " . $new_date);
$new_date = $solarLunar->solarToSimpleLunar($solarDate);
var_dump($solarDate . " 转为阴历, 中文: " . $new_date);
$new_date = $solarLunar->solarToLunar($solarDate, $isLeapMonth);
var_dump("是否是闰月: " . ($isLeapMonth ? "是" : "否"));
var_dump($solarDate . " 转为阴历: " . $new_date);


$lunarDate = "2099-11-25";
$new_date = $solarLunar->lunarToSolar($lunarDate, false);
var_dump($lunarDate . " 转新历为: " . $new_date);

输出:

string(67) "2010-01-05 转为阴历: 冬月廿一日,己丑年 [牛年],月,日"
string(50) "2010-01-05 转为阴历, 中文: 2009年11月21日"
string(20) "是否是闰月: 否"
string(35) "2010-01-05 转为阴历: 2009-11-21"
string(35) "2099-11-25 转新历为: 2100-01-05"

GO 实现方式

https://github.com/nosixtools...


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK