= self::MIN_YEAR && $year <= self::MAX_YEAR; } /** 闰月月号;0 = 该年无闰月 */ public static function leapMonth(int $year): int { return self::INFO[$year - self::MIN_YEAR] & 0xf; } /** 闰月天数(0 / 29 / 30) */ public static function leapDays(int $year): int { if (self::leapMonth($year) === 0) { return 0; } return (self::INFO[$year - self::MIN_YEAR] & 0x10000) ? 30 : 29; } /** 某个月的天数($month 为 1..12,指该月的**非闰**形态) */ public static function monthDays(int $year, int $month): int { if ($month < 1 || $month > 12) { return -1; } return (self::INFO[$year - self::MIN_YEAR] & (0x10000 >> $month)) ? 30 : 29; } /** 农历一年总天数(354~384) */ public static function yearDays(int $year): int { $sum = 348; // 12 × 29 $info = self::INFO[$year - self::MIN_YEAR]; for ($i = 0x8000; $i > 0x8; $i >>= 1) { // bit15..bit4 → 每个大月 +1 if ($info & $i) { $sum++; } } return $sum + self::leapDays($year); } /** * 该农历年的「月序表」:按时间顺序列出每个月。 * 每项 = ['month' => 1..12, 'leap' => bool, 'days' => 29|30] * * 闰月的排法是「紧跟在本月之后」:闰六月 → …六、闰六、七… * (把顺序显式列出来,比在一堆位运算里判断"现在是第几个循环"可靠得多) */ private static function monthSequence(int $year): array { $leap = self::leapMonth($year); $out = []; for ($m = 1; $m <= 12; $m++) { $out[] = ['month' => $m, 'leap' => false, 'days' => self::monthDays($year, $m)]; if ($leap > 0 && $m === $leap) { $out[] = ['month' => $m, 'leap' => true, 'days' => self::leapDays($year)]; } } return $out; } /** 两个公历日期之间相差的天数(一律按 UTC 算,避开历史夏令时把日差算成 0.96 天)*/ private static function daysBetween(string $from, string $to): int { $utc = new \DateTimeZone('UTC'); $a = \DateTimeImmutable::createFromFormat('!Y-m-d', $from, $utc); $b = \DateTimeImmutable::createFromFormat('!Y-m-d', $to, $utc); return (int)$a->diff($b)->days * ($b >= $a ? 1 : -1); } private static function ymd(int $y, int $m, int $d): string { return sprintf('%04d-%02d-%02d', $y, $m, $d); } /** * 阳历 → 阴历。 * @return array{year:int,month:int,day:int,is_leap:bool}|null 越界返回 null */ public static function solarToLunar(int $y, int $m, int $d): ?array { if (!checkdate($m, $d, $y)) { return null; } $offset = self::daysBetween(self::EPOCH, self::ymd($y, $m, $d)); if ($offset < 0) { return null; } // 逐年扣减,定位农历年 $year = self::MIN_YEAR; $temp = 0; while ($year <= self::MAX_YEAR && $offset > 0) { $temp = self::yearDays($year); $offset -= $temp; $year++; } if ($offset < 0) { $offset += $temp; $year--; } if (!self::inRange($year)) { return null; } // 逐月扣减,定位农历月 foreach (self::monthSequence($year) as $mo) { if ($offset < $mo['days']) { return [ 'year' => $year, 'month' => $mo['month'], 'day' => $offset + 1, 'is_leap' => $mo['leap'], ]; } $offset -= $mo['days']; } return null; // 理论上到不了这里 } /** * 阴历 → 阳历。 * @return array{year:int,month:int,day:int}|null 参数非法或越界返回 null */ public static function lunarToSolar(int $ly, int $lm, int $ld, bool $isLeap = false): ?array { if (!self::inRange($ly) || $lm < 1 || $lm > 12 || $ld < 1 || $ld > 30) { return null; } $leap = self::leapMonth($ly); if ($isLeap) { if ($leap !== $lm) { return null; // 该年没有这个闰月 } if ($ld > self::leapDays($ly)) { return null; } } elseif ($ld > self::monthDays($ly, $lm)) { return null; } // 先走到该农历年的正月初一,再走到目标月初一,最后加天数 $offset = 0; for ($y = self::MIN_YEAR; $y < $ly; $y++) { $offset += self::yearDays($y); } foreach (self::monthSequence($ly) as $mo) { if ($mo['month'] === $lm && $mo['leap'] === $isLeap) { $offset += $ld - 1; break; } $offset += $mo['days']; } $utc = new \DateTimeZone('UTC'); $base = \DateTimeImmutable::createFromFormat('!Y-m-d', self::EPOCH, $utc); $got = $base->modify('+' . $offset . ' days'); return [ 'year' => (int)$got->format('Y'), 'month' => (int)$got->format('n'), 'day' => (int)$got->format('j'), ]; } /** 农历月名,如「六月」「闰六月」 */ public static function monthName(int $month, bool $isLeap = false): string { if ($month < 1 || $month > 12) { return ''; } return ($isLeap ? '闰' : '') . self::MONTH_NAMES[$month] . '月'; } /** 农历日名,如「廿六」 */ public static function dayName(int $day): string { return self::DAY_NAMES[$day] ?? ''; } /** * 读起来顺口的农历写法:农历一九五四年冬月廿三。 * 年份用汉字数字——这是老户口本/生辰帖上的写法,长辈看着顺眼。 */ public static function label(int $year, int $month, int $day, bool $isLeap = false): string { return '农历' . self::yearLabel($year) . '年' . self::monthName($month, $isLeap) . self::dayName($day); } /** 年份的汉字写法:1954 → 一九五四 */ public static function yearLabel(int $year): string { $d = ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九']; $s = ''; foreach (str_split((string)$year) as $ch) { $s .= $d[(int)$ch] ?? '〇'; } return $s; } }