PHP阴历阳历天干地支纪年月日生肖五行24节气生辰八字

class Calendar
{
    /**
     * 农历 1900-2100 的润大小信息.
     *
     * @var array
     */
    protected $lunars = [
        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, 0x05ac0, 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, 0x04afb, 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, // 2100
    ];
    /**
     * 公历每个月份的天数表.
     *
     * @var array
     */
    protected $solarMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    /**
     * 天干地支之天干速查表.
     *
     * @var array
     */
    protected $gan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
    /**
     * 天干地支之天干速查表 <=> 色彩.
     *
     * @var array
     */
    protected $colors = ['青', '青', '红', '红', '黄', '黄', '白', '白', '黑', '黑'];
    /**
     * 天干地支之天干速查表 <=> 五行.
     *
     * @var array
     */
    protected $wuXing = ['木', '木', '火', '火', '土', '土', '金', '金', '水', '水'];
    /**
     * 地支 <=> 五行.
     *
     * @var array
     */
    protected $zhiWuxing = ['水', '土', '木', '木', '土', '火', '火', '土', '金', '金', '土', '水'];
    /**
     * 天干地支之地支速查表.
     *
     * @var array
     */
    protected $zhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
    /**
     * 天干地支之地支速查表 <=> 生肖.
     *
     * @var array
     */
    protected $animals = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
    /**
     * 24节气速查表.
     *
     * @var array
     */
    protected $solarTerm = [
        '小寒', '大寒', '立春', '雨水', '惊蛰', '春分',
        '清明', '谷雨', '立夏', '小满', '芒种', '夏至',
        '小暑', '大暑', '立秋', '处暑', '白露', '秋分',
        '寒露', '霜降', '立冬', '小雪', '大雪', '冬至',
    ];
    /**
     * 1900-2100 各年的 24 节气日期速查表.
     *
     * @var array
     */
    protected $solarTerms = [
        '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f',
        '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
        '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa',
        '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f',
        'b027097bd097c36b0b6fc9274c91aa', '9778397bd19801ec9210c965cc920e', '97b6b97bd19801ec95f8c965cc920f',
        '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd197c36c9210c9274c91aa',
        '97b6b97bd19801ec95f8c965cc920e', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2',
        '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bcf97c3598082c95f8e1cfcc920f',
        '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e',
        '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
        '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722',
        '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f',
        '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
        '97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
        '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd07f595b0b6fc920fb0722',
        '9778397bd097c36b0b6fc9210c8dc2', '9778397bd19801ec9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f',
        '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
        '97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
        '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bd07f1487f595b0b0bc920fb0722',
        '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
        '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
        '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
        '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722',
        '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
        '97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
        '97b6b97bd19801ec9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
        '9778397bd097c36b0b6fc9210c91aa', '97b6b97bd197c36c9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722',
        '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
        '97b6b7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
        '9778397bd097c36b0b70c9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
        '7f0e397bd097c35b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
        '7f0e27f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
        '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
        '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
        '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
        '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
        '97b6b7f0e47f531b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
        '9778397bd097c36b0b6fc9210c91aa', '97b6b7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
        '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '977837f0e37f149b0723b0787b0721',
        '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c35b0b6fc9210c8dc2',
        '977837f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
        '7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
        '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd',
        '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
        '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
        '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
        '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd',
        '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
        '977837f0e37f14998082b0723b06bd', '7f07e7f0e37f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
        '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721',
        '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5',
        '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f531b0b0bb0b6fb0722',
        '7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
        '7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
        '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35',
        '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
        '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721',
        '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd',
        '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35',
        '7ec967f0e37f14998082b0723b06bd', '7f07e7f0e37f14998083b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
        '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14898082b0723b02d5', '7f07e7f0e37f14998082b0787b0721',
        '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66aa89801e9808297c35', '665f67f0e37f14898082b0723b02d5',
        '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66a449801e9808297c35',
        '665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
        '7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
        '7f07e7f0e47f531b0723b0b6fb0721', '7f0e26665b66a449801e9808297c35', '665f67f0e37f1489801eb072297c35',
        '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
    ];
    /**
     * 数字转中文速查表.
     *
     * @var array
     */
    protected $weekdayAlias = ['日', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
    /**
     * 日期转农历称呼速查表.
     *
     * @var array
     */
    protected $dateAlias = ['初', '十', '廿', '卅'];
    /**
     * 月份转农历称呼速查表.
     *
     * @var array
     */
    protected $monthAlias = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];
    /**
     * 传入阳历年月日获得详细的公历、农历信息.
     *
     * @param int $year
     * @param int $month
     * @param int $day
     * @param int $hour
     *
     * @return array
     */
    public function solar($year, $month, $day, $hour = null)
    {
        $date = $this->makeDate("{$year}-{$month}-{$day}");
        $lunar = $this->solar2lunar($year, $month, $day, $hour);
        $week = abs($date->format('w')); // 0 ~ 6 修正 星期七 为 星期日
        return array_merge($lunar, [
            'gregorian_year' => (string) $year,
            'gregorian_month' => sprintf('%02d', $month),
            'gregorian_day' => sprintf('%02d', $day),
            'gregorian_hour' => !is_numeric($hour) || $hour < 0 || $hour > 23 ? null : sprintf('%02d', $hour),
            'week_no' => $week, // 在周日时将会传回 0
            'week_name' => '星期'.$this->weekdayAlias[$week],
            'is_today' => 0 === $this->makeDate('now')->diff($date)->days,
            'constellation' => $this->toConstellation($month, $day),
        ]);
    }
    /**
     * 传入农历年月日以及传入的月份是否闰月获得详细的公历、农历信息.
     *
     * @param int  $year        lunar year
     * @param int  $month       lunar month
     * @param int  $day         lunar day
     * @param bool $isLeapMonth lunar month is leap or not.[如果是农历闰月第四个参数赋值true即可]
     * @param int  $hour        birth hour.[0~23]
     *
     * @return array
     */
    public function lunar($year, $month, $day, $isLeapMonth = false, $hour = null)
    {
        $solar = $this->lunar2solar($year, $month, $day, $isLeapMonth);
        return $this->solar($solar['solar_year'], $solar['solar_month'], $solar['solar_day'], $hour);
    }
    /**
     * 返回农历指定年的总天数.
     *
     * @param int $year
     *
     * @return int
     */
    public function daysOfYear($year)
    {
        $sum = 348;
        for ($i = 0x8000; $i > 0x8; $i >>= 1) {
            $sum += ($this->lunars[$year - 1900] & $i) ? 1 : 0;
        }
        return $sum + $this->leapDays($year);
    }
    /**
     * 返回农历指定年的总月数.
     *
     * @param int $year
     *
     * @return int
     */
    public function monthsOfYear($year)
    {
        return 0 < $this->leapMonth($year) ? 13 : 12;
    }
    /**
     * 返回农历 y 年闰月是哪个月;若 y 年没有闰月 则返回0.
     *
     * @param int $year
     *
     * @return int
     */
    public function leapMonth($year)
    {
        // 闰字编码 \u95f0
        return $this->lunars[$year - 1900] & 0xf;
    }
    /**
     * 返回农历y年闰月的天数 若该年没有闰月则返回 0.
     *
     * @param int $year
     *
     * @return int
     */
    public function leapDays($year)
    {
        if ($this->leapMonth($year)) {
            return ($this->lunars[$year - 1900] & 0x10000) ? 30 : 29;
        }
        return 0;
    }
    /**
     * 返回农历 y 年 m 月(非闰月)的总天数,计算 m 为闰月时的天数请使用 leapDays 方法.
     *
     * @param int $year
     * @param int $month
     *
     * @return int
     */
    public function lunarDays($year, $month)
    {
        // 月份参数从 1 至 12,参数错误返回 -1
        if ($month > 12 || $month < 1) {
            return -1;
        }
        return ($this->lunars[$year - 1900] & (0x10000 >> $month)) ? 30 : 29;
    }
    /**
     * 返回公历 y 年 m 月的天数.
     *
     * @param int $year
     * @param int $month
     *
     * @return int
     */
    public function solarDays($year, $month)
    {
        // 若参数错误 返回-1
        if ($month > 12 || $month < 1) {
            return -1;
        }
        $ms = $month - 1;
        if (1 == $ms) { // 2 月份的闰平规律测算后确认返回 28 或 29
            return ((0 === $year % 4) && (0 !== $year % 100) || (0 === $year % 400)) ? 29 : 28;
        }
        return $this->solarMonth[$ms];
    }
    /**
     * 农历年份转换为干支纪年.
     *
     * @param int      $lunarYear
     * @param null|int $termIndex
     *
     * @return string
     */
    public function ganZhiYear($lunarYear, $termIndex = null)
    {
        /**
         * 据维基百科干支词条:『在西历新年后,华夏新年或干支历新年之前,则续用上一年之干支』
         * 所以干支年份应该不需要根据节气校正,为免影响现有系统,此处暂时保留原有逻辑
         * https://zh.wikipedia.org/wiki/%E5%B9%B2%E6%94%AF.
         *
         * 即使考虑节气,有的年份没有立春,有的年份有两个立春,此处逻辑仍不能处理该特殊情况
         */
        $adjust = null !== $termIndex && 3 > $termIndex ? 1 : 0;
        $ganKey = ($lunarYear + $adjust - 4) % 10;
        $zhiKey = ($lunarYear + $adjust - 4) % 12;
        return $this->gan[$ganKey].$this->zhi[$zhiKey];
    }
    /**
     * 公历月、日判断所属星座.
     *
     * @param int $gregorianMonth
     * @param int $gregorianDay
     *
     * @return string
     */
    public function toConstellation($gregorianMonth, $gregorianDay)
    {
        $constellations = '魔羯水瓶双鱼白羊金牛双子巨蟹狮子处女天秤天蝎射手魔羯';
        $arr = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22];
        return mb_substr(
            $constellations,
            $gregorianMonth * 2 - ($gregorianDay < $arr[$gregorianMonth - 1] ? 2 : 0),
            2,
            'UTF-8'
        );
    }
    /**
     * 传入offset偏移量返回干支.
     *
     * @param int $offset 相对甲子的偏移量
     *
     * @return string
     */
    public function toGanZhi($offset)
    {
        return $this->gan[$offset % 10].$this->zhi[$offset % 12];
    }
    /**
     * 传入公历年获得该年第n个节气的公历日期
     *
     * @example
     * <pre>
     *  $_24 = $this->getTerm(1987,3) ;// _24 = 4; 意即 1987 年 2 月 4 日立春
     * </pre>
     *
     * @param int $year 公历年(1900-2100);
     * @param int $no   二十四节气中的第几个节气(1~24);从n=1(小寒)算起
     *
     * @return int
     */
    public function getTerm($year, $no)
    {
        if ($year < 1900 || $year > 2100) {
            return -1;
        }
        if ($no < 1 || $no > 24) {
            return -1;
        }
        $solarTermsOfYear = array_map('hexdec', str_split($this->solarTerms[$year - 1900], 5));
        $positions = [
            0 => [0, 1],
            1 => [1, 2],
            2 => [3, 1],
            3 => [4, 2],
        ];
        $group = intval(($no - 1) / 4);
        list($offset, $length) = $positions[($no - 1) % 4];
        return substr($solarTermsOfYear[$group], $offset, $length);
    }
    public function toChinaYear($year)
    {
        if (!is_numeric($year)) {
            throw new InvalidArgumentException("错误的年份:{$year}");
        }
        $lunarYear = '';
        $year = (string) $year;
        for ($i = 0, $l = strlen($year); $i < $l; ++$i) {
            $lunarYear .= '0' !== $year[$i] ? $this->weekdayAlias[$year[$i]] : '零';
        }
        return $lunarYear;
    }
    /**
     * 传入农历数字月份返回汉语通俗表示法.
     *
     * @param int $month
     *
     * @return string
     */
    public function toChinaMonth($month)
    {
        // 若参数错误 返回 -1
        if ($month > 12 || $month < 1) {
            throw new InvalidArgumentException("错误的月份:{$month}");
        }
        return $this->monthAlias[abs($month) - 1].'月';
    }
    /**
     * 传入农历日期数字返回汉字表示法.
     *
     * @param int $day
     *
     * @return string
     */
    public function toChinaDay($day)
    {
        switch ($day) {
            case 10:
                return '初十';
            case 20:
                return '二十';
            case 30:
                return '三十';
            default:
                return $this->dateAlias[intval($day / 10)].$this->weekdayAlias[$day % 10];
        }
    }
    /**
     * 年份转生肖.
     *
     * 仅能大致转换, 精确划分生肖分界线是 “立春”.
     *
     * @param int      $year
     * @param null|int $termIndex
     *
     * @return string
     */
    public function getAnimal($year, $termIndex = null)
    {
        // 认为此逻辑不需要,详情参见 ganZhiYear 相关注释
        $adjust = null !== $termIndex && 3 > $termIndex ? 1 : 0;
        $animalIndex = ($year + $adjust - 4) % 12;
        return $this->animals[$animalIndex];
    }
    /**
     * 干支转色彩.
     *
     * @param $ganZhi
     *
     * @return string
     */
    protected function getColor($ganZhi)
    {
        if (!$ganZhi) {
            return null;
        }
        $gan = substr($ganZhi, 0, 3);
        if (!$gan) {
            return null;
        }
        return $this->colors[array_search($gan, $this->gan)];
    }
    /**
     * 干支转五行.
     *
     * @param $ganZhi
     *
     * @return string
     */
    protected function getWuXing($ganZhi)
    {
        if (!$ganZhi) {
            return null;
        }
        $gan = substr($ganZhi, 0, 3);
        $zhi = substr($ganZhi, 3);
        if (!$gan || !$zhi) {
            return null;
        }
        $wGan = $this->wuXing[array_search($gan, $this->gan)];
        $wZhi = $this->zhiWuxing[array_search($zhi, $this->zhi)];
        return $wGan.$wZhi;
    }
    /**
     * 阳历转阴历.
     *
     * @param int $year
     * @param int $month
     * @param int $day
     * @param int $hour
     *
     * @return array
     */
    public function solar2lunar($year, $month, $day, $hour = null)
    {
        if (23 == $hour) {
            // 23点过后算子时,农历以子时为一天的起始
            $date = $this->makeDate("{$year}-{$month}-{$day} +1day");
        } else {
            $date = $this->makeDate("{$year}-{$month}-{$day}");
        }
        list($year, $month, $day) = explode('-', $date->format('Y-n-j'));
        // 参数区间1900.1.31~2100.12.31
        if ($year < 1900 || $year > 2100) {
            throw new InvalidArgumentException("不支持的年份:{$year}");
        }
        // 年份限定、上限
        if (1900 == $year && 1 == $month && $day < 31) {
            throw new InvalidArgumentException("不支持的日期:{$year}-{$month}-{$day}");
        }
        $offset = $this->dateDiff($date, '1900-01-31')->days;
        for ($i = 1900; $i < 2101 && $offset > 0; ++$i) {
            $daysOfYear = $this->daysOfYear($i);
            $offset -= $daysOfYear;
        }
        if ($offset < 0) {
            $offset += $daysOfYear;
            --$i;
        }
        // 农历年
        $lunarYear = $i;
        $leap = $this->leapMonth($i); // 闰哪个月
        $isLeap = false;
        // 用当年的天数 offset,逐个减去每月(农历)的天数,求出当天是本月的第几天
        for ($i = 1; $i < 13 && $offset > 0; ++$i) {
            // 闰月
            if ($leap > 0 && $i == ($leap + 1) && !$isLeap) {
                --$i;
                $isLeap = true;
                $daysOfMonth = $this->leapDays($lunarYear); // 计算农历月天数
            } else {
                $daysOfMonth = $this->lunarDays($lunarYear, $i); // 计算农历普通月天数
            }
            // 解除闰月
            if (true === $isLeap && $i == ($leap + 1)) {
                $isLeap = false;
            }
            $offset -= $daysOfMonth;
        }
        // offset为0时,并且刚才计算的月份是闰月,要校正
        if (0 === $offset && $leap > 0 && $i == $leap + 1) {
            if ($isLeap) {
                $isLeap = false;
            } else {
                $isLeap = true;
                --$i;
            }
        }
        if ($offset < 0) {
            $offset += $daysOfMonth;
            --$i;
        }
        // 农历月
        $lunarMonth = $i;
        // 农历日
        $lunarDay = $offset + 1;
        // 月柱 1900 年 1 月小寒以前为 丙子月(60进制12)
        $firstNode = $this->getTerm($year, ($month * 2 - 1)); // 返回当月「节气」为几日开始
        $secondNode = $this->getTerm($year, ($month * 2)); // 返回当月「节气」为几日开始
        // 依据 12 节气修正干支月
        $ganZhiMonth = $this->toGanZhi(($year - 1900) * 12 + $month + 11);
        if ($day >= $firstNode) {
            $ganZhiMonth = $this->toGanZhi(($year - 1900) * 12 + $month + 12);
        }
        // 获取该天的节气
        $termIndex = null;
        if ($firstNode == $day) {
            $termIndex = $month * 2 - 2;
        }
        if ($secondNode == $day) {
            $termIndex = $month * 2 - 1;
        }
        $term = null !== $termIndex ? $this->solarTerm[$termIndex] : null;
        // 日柱 当月一日与 1900/1/1 相差天数
        $dayCyclical = $this->dateDiff("{$year}-{$month}-01", '1900-01-01')->days + 10;
        $dayCyclical += $day - 1;
        $ganZhiDay = $this->toGanZhi($dayCyclical);
        // 时柱和时辰
        list($ganZhiHour, $lunarHour, $hour) = $this->ganZhiHour($hour, $dayCyclical);
        $ganZhiYear = $this->ganZhiYear($lunarYear, $termIndex);
        return [
            'lunar_year' => (string) $lunarYear,
            'lunar_month' => sprintf('%02d', $lunarMonth),
            'lunar_day' => sprintf('%02d', $lunarDay),
            'lunar_hour' => $hour,
            'lunar_year_chinese' => $this->toChinaYear($lunarYear),
            'lunar_month_chinese' => ($isLeap ? '闰' : '').$this->toChinaMonth($lunarMonth),
            'lunar_day_chinese' => $this->toChinaDay($lunarDay),
            'lunar_hour_chinese' => $lunarHour,
            'ganzhi_year' => $ganZhiYear,
            'ganzhi_month' => $ganZhiMonth,
            'ganzhi_day' => $ganZhiDay,
            'ganzhi_hour' => $ganZhiHour,
            'wuxing_year' => $this->getWuXing($ganZhiYear),
            'wuxing_month' => $this->getWuXing($ganZhiMonth),
            'wuxing_day' => $this->getWuXing($ganZhiDay),
            'wuxing_hour' => $this->getWuXing($ganZhiHour),
            'color_year' => $this->getColor($ganZhiYear),
            'color_month' => $this->getColor($ganZhiMonth),
            'color_day' => $this->getColor($ganZhiDay),
            'color_hour' => $this->getColor($ganZhiHour),
            'animal' => $this->getAnimal($lunarYear, $termIndex),
            'term' => $term,
            'is_leap' => $isLeap,
        ];
    }
    /**
     * 阴历转阳历.
     *
     * @param int  $year
     * @param int  $month
     * @param int  $day
     * @param bool $isLeapMonth
     *
     * @return array|int
     */
    public function lunar2solar($year, $month, $day, $isLeapMonth = false)
    {
        // 参数区间 1900.1.3 1 ~2100.12.1
        $leapMonth = $this->leapMonth($year);
        // 传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同
        if ($isLeapMonth && ($leapMonth != $month)) {
            $isLeapMonth = false;
        }
        // 超出了最大极限值
        if (2100 == $year && 12 == $month && $day > 1 || 1900 == $year && 1 == $month && $day < 31) {
            return -1;
        }
        $maxDays = $days = $this->lunarDays($year, $month);
        // if month is leap, _day use leapDays method
        if ($isLeapMonth) {
            $maxDays = $this->leapDays($year, $month);
        }
        // 参数合法性效验
        if ($year < 1900 || $year > 2100 || $day > $maxDays) {
            throw new InvalidArgumentException('传入的参数不合法');
        }
        // 计算农历的时间差
        $offset = 0;
        for ($i = 1900; $i < $year; ++$i) {
            $offset += $this->daysOfYear($i);
        }
        $isAdd = false;
        for ($i = 1; $i < $month; ++$i) {
            $leap = $this->leapMonth($year);
            if (!$isAdd) {// 处理闰月
                if ($leap <= $i && $leap > 0) {
                    $offset += $this->leapDays($year);
                    $isAdd = true;
                }
            }
            $offset += $this->lunarDays($year, $i);
        }
        // 转换闰月农历 需补充该年闰月的前一个月的时差
        if ($isLeapMonth) {
            $offset += $days;
        }
        // 1900 年农历正月一日的公历时间为 1900 年 1 月 30 日 0 时 0 分 0 秒 (该时间也是本农历的最开始起始点)
        // XXX: 部分 windows 机器不支持负时间戳,所以这里就写死了,哈哈哈哈...
        $startTimestamp = -2206483200;
        $date = date('Y-m-d', ($offset + $day) * 86400 + $startTimestamp);
        list($solarYear, $solarMonth, $solarDay) = explode('-', $date);
        return [
            'solar_year' => $solarYear,
            'solar_month' => sprintf('%02d', $solarMonth),
            'solar_day' => sprintf('%02d', $solarDay),
        ];
    }
    /**
     * 获取两个日期之间的距离.
     *
     * @param string|\DateTime $date1
     * @param string|\DateTime $date2
     *
     * @return bool|\DateInterval
     */
    public function dateDiff($date1, $date2)
    {
        if (!($date1 instanceof DateTime)) {
            $date1 = $this->makeDate($date1);
        }
        if (!($date2 instanceof DateTime)) {
            $date2 = $this->makeDate($date2);
        }
        return $date1->diff($date2);
    }
    /**
     * 获取两个日期之间以年为单位的距离.
     *
     * @param array $lunar1
     * @param array $lunar2
     * @param bool  $absolute
     *
     * @return int
     */
    public function diffInYears($lunar1, $lunar2, $absolute = true)
    {
        $solar1 = $this->lunar2solar($lunar1['lunar_year'], $lunar1['lunar_month'], $lunar1['lunar_day'], $lunar1['is_leap']);
        $date1 = $this->makeDate("{$solar1['solar_year']}-{$solar1['solar_month']}-{$solar1['solar_day']}");
        $solar2 = $this->lunar2solar($lunar2['lunar_year'], $lunar2['lunar_month'], $lunar2['lunar_day'], $lunar2['is_leap']);
        $date2 = $this->makeDate("{$solar2['solar_year']}-{$solar2['solar_month']}-{$solar2['solar_day']}");
        if ($date1 < $date2) {
            $lessLunar = $lunar1;
            $greaterLunar = $lunar2;
            $changed = false;
        } else {
            $lessLunar = $lunar2;
            $greaterLunar = $lunar1;
            $changed = true;
        }
        $monthAdjustFactor = $greaterLunar['lunar_day'] >= $lessLunar['lunar_day'] ? 0 : 1;
        if ($greaterLunar['lunar_month'] == $lessLunar['lunar_month']) {
            if ($greaterLunar['is_leap'] && !$lessLunar['is_leap']) {
                $monthAdjustFactor = 0;
            } elseif (!$greaterLunar['is_leap'] && $lessLunar['is_leap']) {
                $monthAdjustFactor = 1;
            }
        }
        $yearAdjustFactor = $greaterLunar['lunar_month'] - $monthAdjustFactor >= $lessLunar['lunar_month'] ? 0 : 1;
        $diff = $greaterLunar['lunar_year'] - $yearAdjustFactor - $lessLunar['lunar_year'];
        return $absolute ? $diff : ($changed ? -1 * $diff : $diff);
    }
    /**
     * 获取两个日期之间以月为单位的距离.
     *
     * @param array $lunar1
     * @param array $lunar2
     * @param bool  $absolute
     *
     * @return int
     */
    public function diffInMonths($lunar1, $lunar2, $absolute = true)
    {
        $solar1 = $this->lunar2solar($lunar1['lunar_year'], $lunar1['lunar_month'], $lunar1['lunar_day'], $lunar1['is_leap']);
        $date1 = $this->makeDate("{$solar1['solar_year']}-{$solar1['solar_month']}-{$solar1['solar_day']}");
        $solar2 = $this->lunar2solar($lunar2['lunar_year'], $lunar2['lunar_month'], $lunar2['lunar_day'], $lunar2['is_leap']);
        $date2 = $this->makeDate("{$solar2['solar_year']}-{$solar2['solar_month']}-{$solar2['solar_day']}");
        if ($date1 < $date2) {
            $lessLunar = $lunar1;
            $greaterLunar = $lunar2;
            $changed = false;
        } else {
            $lessLunar = $lunar2;
            $greaterLunar = $lunar1;
            $changed = true;
        }
        $diff = 0;
        if ($lessLunar['lunar_year'] == $greaterLunar['lunar_year']) {
            $leapMonth = $this->leapMonth($lessLunar['lunar_year']);
            $lessLunarAdjustFactor = $lessLunar['is_leap'] || (0 < $leapMonth && $leapMonth < $lessLunar['lunar_month']) ? 1 : 0;
            $greaterLunarAdjustFactor = $greaterLunar['is_leap'] || (0 < $leapMonth && $leapMonth < $greaterLunar['lunar_month']) ? 1 : 0;
            $diff = $greaterLunar['lunar_month'] + $greaterLunarAdjustFactor - $lessLunar['lunar_month'] - $lessLunarAdjustFactor;
        } else {
            $lessLunarLeapMonth = $this->leapMonth($lessLunar['lunar_year']);
            $greaterLunarLeapMonth = $this->leapMonth($greaterLunar['lunar_year']);
            $lessLunarAdjustFactor = (!$lessLunar['is_leap'] && $lessLunarLeapMonth == $lessLunar['lunar_month']) || $lessLunarLeapMonth > $lessLunar['lunar_month'] ? 1 : 0;
            $diff += 12 + $lessLunarAdjustFactor - $lessLunar['lunar_month'];
            for ($i = $lessLunar['lunar_year'] + 1; $i < $greaterLunar['lunar_year']; ++$i) {
                $diff += $this->monthsOfYear($i);
            }
            $greaterLunarAdjustFactor = $greaterLunar['is_leap'] || (0 < $greaterLunarLeapMonth && $greaterLunarLeapMonth < $greaterLunar['lunar_month']) ? 1 : 0;
            $diff += $greaterLunarAdjustFactor + $greaterLunar['lunar_month'];
        }
        $diff -= $greaterLunar['lunar_day'] >= $lessLunar['lunar_day'] ? 0 : 1;
        return $absolute ? $diff : ($changed ? -1 * $diff : $diff);
    }
    /**
     * 获取两个日期之间以日为单位的距离.
     *
     * @param array $lunar1
     * @param array $lunar2
     * @param bool  $absolute
     *
     * @return int
     */
    public function diffInDays($lunar1, $lunar2, $absolute = true)
    {
        $solar1 = $this->lunar2solar($lunar1['lunar_year'], $lunar1['lunar_month'], $lunar1['lunar_day'], $lunar1['is_leap']);
        $date1 = $this->makeDate("{$solar1['solar_year']}-{$solar1['solar_month']}-{$solar1['solar_day']}");
        $solar2 = $this->lunar2solar($lunar2['lunar_year'], $lunar2['lunar_month'], $lunar2['lunar_day'], $lunar2['is_leap']);
        $date2 = $this->makeDate("{$solar2['solar_year']}-{$solar2['solar_month']}-{$solar2['solar_day']}");
        return $date1->diff($date2, $absolute)->format('%r%a');
    }
    /**
     * 增加年数.
     *
     * @param array $lunar
     * @param int   $value
     * @param bool  $overFlow
     *
     * @return array
     */
    public function addYears($lunar, $value = 1, $overFlow = true)
    {
        $newYear = $lunar['lunar_year'] + $value;
        $newMonth = $lunar['lunar_month'];
        $newDay = $lunar['lunar_day'];
        $isLeap = $lunar['is_leap'];
        $needOverFlow = false;
        $leapMonth = $this->leapMonth($newYear);
        $isLeap = $isLeap && $newMonth == $leapMonth;
        $maxDays = $isLeap ? $this->leapDays($newYear) : $this->lunarDays($newYear, $newMonth);
        if ($newDay > $maxDays) {
            if ($overFlow) {
                $newDay = 1;
                $needOverFlow = true;
            } else {
                $newDay = $maxDays;
            }
        }
        $ret = $this->lunar($newYear, $newMonth, $newDay, $isLeap);
        if ($needOverFlow) {
            $ret = $this->addMonths($ret, 1, $overFlow);
        }
        return $ret;
    }
    /**
     * 减少年数.
     *
     * @param array $lunar
     * @param int   $value
     * @param bool  $overFlow
     *
     * @return array
     */
    public function subYears($lunar, $value = 1, $overFlow = true)
    {
        return $this->addYears($lunar, -1 * $value, $overFlow);
    }
    /**
     * 增加月数.
     *
     * @param array $lunar
     * @param int   $value
     * @param bool  $overFlow
     *
     * @return array
     */
    public function addMonths($lunar, $value = 1, $overFlow = true)
    {
        if (0 > $value) {
            return $this->subMonths($lunar, -1 * $value, $overFlow);
        } else {
            $newYear = $lunar['lunar_year'];
            $newMonth = $lunar['lunar_month'];
            $newDay = $lunar['lunar_day'];
            $isLeap = $lunar['is_leap'];
            while (0 < $value) {
                $leapMonth = $this->leapMonth($newYear);
                if (0 < $leapMonth) {
                    $currentIsLeap = $isLeap;
                    $isLeap = $newMonth + $value == $leapMonth + ($isLeap ? 0 : 1);
                    if ((!$currentIsLeap && $leapMonth == $newMonth) || ($newMonth < $leapMonth && $newMonth + $value > $leapMonth)) {
                        --$value;
                    }
                } else {
                    $isLeap = false;
                }
                if (13 > $newMonth + $value) {
                    $newMonth += $value;
                    $value = 0;
                } else {
                    $value = $value + $newMonth - 13;
                    ++$newYear;
                    $newMonth = 1;
                }
                if (0 == $value) {
                    $maxDays = $isLeap ? $this->leapDays($newYear) : $this->lunarDays($newYear, $newMonth);
                    if ($newDay > $maxDays) {
                        if ($overFlow) {
                            $newDay = 1;
                            ++$value;
                        } else {
                            $newDay = $maxDays;
                        }
                    }
                }
            }
            return $this->lunar($newYear, $newMonth, $newDay, $isLeap);
        }
    }
    /**
     * 减少月数.
     *
     * @param array $lunar
     * @param int   $value
     * @param bool  $overFlow
     *
     * @return array
     */
    public function subMonths($lunar, $value = 1, $overFlow = true)
    {
        if (0 > $value) {
            return $this->addMonths($lunar, -1 * $value, $overFlow);
        } else {
            $newYear = $lunar['lunar_year'];
            $newMonth = $lunar['lunar_month'];
            $newDay = $lunar['lunar_day'];
            $isLeap = $lunar['is_leap'];
            $needOverFlow = false;
            while (0 < $value) {
                $leapMonth = $this->leapMonth($newYear);
                if (0 < $leapMonth) {
                    $isLeap = $newMonth - $value == $leapMonth;
                    if ($newMonth >= $leapMonth && $newMonth - $value < $leapMonth) {
                        --$value;
                    }
                } else {
                    $isLeap = false;
                }
                if ($newMonth > $value) {
                    $newMonth -= $value;
                    $value = 0;
                } else {
                    $value = $value - $newMonth;
                    --$newYear;
                    $newMonth = 12;
                }
                if (0 == $value) {
                    $maxDays = $isLeap ? $this->leapDays($newYear) : $this->lunarDays($newYear, $newMonth);
                    if ($newDay > $maxDays) {
                        $newDay = $maxDays;
                        $needOverFlow = $overFlow;
                    }
                }
            }
            $ret = $this->lunar($newYear, $newMonth, $newDay, $isLeap);
            if ($needOverFlow) {
                $ret = $this->addDays($ret, 1);
            }
            return $ret;
        }
    }
    /**
     * 增加天数.
     *
     * @param array $lunar
     * @param int   $value
     *
     * @return array
     */
    public function addDays($lunar, $value = 1)
    {
        $solar = $this->lunar2solar($lunar['lunar_year'], $lunar['lunar_month'], $lunar['lunar_day'], $lunar['is_leap']);
        $date = $this->makeDate("{$solar['solar_year']}-{$solar['solar_month']}-{$solar['solar_day']}");
        $date->modify($value.' day');
        return $this->solar2lunar($date->format('Y'), $date->format('m'), $date->format('d'));
    }
    /**
     * 减少天数.
     *
     * @param array $lunar
     * @param int   $value
     *
     * @return array
     */
    public function subDays($lunar, $value = 1)
    {
        return $this->addDays($lunar, -1 * $value);
    }
    /**
     * 创建日期对象
     *
     * @param string $string
     * @param string $timezone
     *
     * @return \DateTime
     */
    protected function makeDate($string = 'now', $timezone = 'UTC')
    {
    	// $timezone 不能用PRC 否则会有bug 例:1987-04-12和1987-04-13 距 1900-01-31天数相同 31847天
        return new DateTime($string, new DateTimeZone($timezone));
    }
    /**
     * 获取时柱.
     *
     * @param int $hour      0~23 小时格式
     * @param int $ganZhiDay 干支日期
     *
     * @return array
     *
     * @see https://baike.baidu.com/item/%E6%97%B6%E6%9F%B1/6274024
     */
    protected function ganZhiHour($hour, $ganZhiDay)
    {
        if (!is_numeric($hour) || $hour < 0 || $hour > 23) {
            return [null, null, null];
        }
        $zhiHour = intval(($hour + 1) / 2);
        $zhiHour = 12 === $zhiHour ? 0 : $zhiHour;
        return [
            $this->gan[($ganZhiDay % 10 % 5 * 2 + $zhiHour) % 10].$this->zhi[$zhiHour],
            $this->zhi[$zhiHour].'时',
            sprintf('%02d', $hour),
        ];
    }
}

如果使用命名空间,代码中所有DateTime前加‘\’

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值