PHP 实现农历日历转换算法详解

沿途有光
沿途有光
发布时间:2026-10-13 | 6人浏览过
原创

本文展示了一种基于预置数据表的 PHP 农历转换算法。该函数接收公历年和月作为输入,返回当月每一天的农历表示(如“初一”、“七月”)以及当年的干支纪年。核心逻辑依赖于对农历每月天数、闰月规则的预设数组进行累加比对,从而确定当前的农历月份和日期。

农历转换算法原理

农历是一种阴阳合历,其月份大小(大月30天,小月29天)以及是否有闰月均由天文观测决定,没有简单的数学公式可推导。因此,此类算法通常采用查表法:预先存储特定年份范围内的每个月的天数、闰月位置以及对应的干支序数,然后通过计算公历与基准日期的天数差,在表中逐月累加定位。

数据结构设计

算法的核心在于两个主要的数据结构:年度数据表和农历用字映射表。

1. 年度数据表 $everymonth

这是一个多维数组,索引为相对于基准年份的偏移量。每个元素是一个一维数组,结构如下:

  • [0]: 闰月月份,若无闰月则为 0。
  • [1] ~ [13]: 全年各月的天数。若有闰月,则第 [0] 位之后会插入额外的一个月数据。
  • [14]: 当年天干在天干数组中的索引(1-10)。
  • [15]: 当地地在地支数组中的索引(1-12)。

例如,$everymonth[0] 代表某一年,其数据为 array(8, 0, 0, ..., 29, 30, 7, 1),表示该年闰八月,最后两个月分别为29天和30天,天干索引7,地支索引1。

2. 农历用字映射表 $lnlunarcalendar

定义了中文汉字与农历概念的对应关系:

  • 'tiangan': 甲、乙、丙... 癸。
  • 'dizhi': 子(鼠)、丑(牛)... 亥(猪),并附带生肖备注。
  • 'month': 正、二、三... 十二,以及“闰”字前缀和“月”字后缀。
  • 'day': 初一 至 三十的中文表示。

操作步骤与逻辑流程

第1步:初始化与参数校验

定义全局变量引用农历用字表。设定基准时间为公元1900年12月21日之前的虚拟累计天数(69 * 365 + 17 + 11),用于后续计算阳历总天数。

函数接收 $year 和 $month 参数。若输入为空或年份超出 1970 至 2020 范围,直接返回空字符串,确保数据有效性。

第2步:计算目标公历总天数

从基准年1970年开始遍历至目标年份的前一年:

  • 每年默认加365天。
  • 若该年是闰年($y % 4 == 0),额外加1天。

接着,利用 gmdate("z", gmmktime(...)) 获取目标年份目标月份之前的累计天数,加上上述年累计值,得到目标日期相对于1900年基准日的总阳历天数 $total。

第3步:查找对应的农历年月

使用双重循环遍历 $everymonth 表:

  1. 外层循环 $lcj 逐年增加,内层循环 $lci 逐月累加天数 $mtotal。
  2. 当 $mtotal >= $total 时,说明已跨越目标日期,跳出循环。

此时:
- $lcj 指向目标年份在表中的索引。
- $lci 指向目标月份在表中的索引。
- $fisrtdaylunar 通过计算当月首日与目标日的农历日期偏移量得出:$everymonth[$lcj][$lci] - ($mtotal - $total)。

第4步:生成当月每日的农历结果

遍历目标公历月的每一天(1 到 daysthismonth):

  1. 日期转换:根据 $fisrtdaylunar 递增,利用 $lnlunarcalendar['day'] 映射出“初一”、“初二”等。
  2. 月份更替判断:当农历日期超过当前月最大天数时,重置为初一,并进入下一个月。
  3. 闰年/跨年处理:
    • 检查当前年是否有闰月($everymonth[$lcj][0] != 0)。
    • 若当前月索引超过12个月,需判断是否进入下一年。若进入下一年,更新 $results['year'] 为新年的干支。
    • 特殊处理闰月:若当月是闰月,显示为“闰X月”;若是闰月之后的月份,索引需减1以正确映射汉字。

第5步:返回结果集

返回一个关联数组,其中:
- $results['year']: 完整的干支纪年字符串,如“乙未年(羊)”。
- $results[1..31]: 对应公历日期 1 日至 31 日的农历表示,如“十七”、“七月”等。

代码实现示例

<?php
function lunarcalendar ($month, $year)
{
 global $lnlunarcalendar;
 /**
  * Lunar calendar 博大精深的农历
  * 原始数据和算法思路来自 S&S
  */
 /*
  农历每月的天数。
  每个元素为一年。每个元素中的数据为:
  [0]是闰月在哪个月,0为无闰月;
  [1]到[13]是每年12或13个月的每月天数;
  [14]是当年的天干次序,
  [15]是当年的地支次序
  */
 $everymonth = array(
   0 => array(8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 30, 7, 1),
   1 => array(0, 29, 30, 29, 29, 30, 29, 30, 29, 30, 30, 30, 29, 0, 8, 2),
   // ... 此处省略中间年份数据,实际使用中需保留完整数组 ...
   120 => array(4, 29, 30, 30, 30, 29, 30, 29, 29, 30, 29, 30, 29, 30, 7, 1)
 );
 
 $mten = $lnlunarcalendar['tiangan'];// 农历天干
 $mtwelve = $lnlunarcalendar['dizhi'];// 农历地支
 $mmonth = $lnlunarcalendar['month'];// 农历月份
 $mday = $lnlunarcalendar['day'];// 农历日
 
 // 阳历总天数 至1900年12月21日
 $total = 69 * 365 + 17 + 11;
 
 //1970年1月1日前的就不算了
 if ($year == "" || $month == "" || ($year < 1970 or $year > 2020)) return ''; 
 
 // 计算到所求日期阳历的总天数-自1900年12月21日始
 for ($y = 1970; $y < $year;$y++) {// 先算年的和
  $total += 365;
  if ($y % 4 == 0) $total ++;
 }
 // 再加当年的几个月
 $total += gmdate("z", gmmktime(0, 0, 0, $month, 1, $year));
 
 // 用农历的天数累加来判断是否超过阳历的天数
 $flag1 = 0; //判断跳出循环的条件
 $lcj = 0;
 while ($lcj <= 120) {
  $lci = 1;
  while ($lci <= 13) {
   @$mtotal += $everymonth[$lcj][$lci];
   if ($mtotal >= $total) {
    $flag1 = 1;
    break;
   }
   $lci++;
  }
  if ($flag1 == 1) break;
  $lcj++;
 }
 
 // 由上,得到的 $lci 为当前农历月, $lcj 为当前农历年
 // 计算所求月份1号的农历日期
 $fisrtdaylunar = $everymonth[$lcj][$lci] - ($mtotal - $total);
 $results['year'] = $mten[$everymonth[$lcj][14]] . $mtwelve[$everymonth[$lcj][15]]; 
 $daysthismonth = gmdate("t", gmmktime(0, 0, 0, $month, 1, $year)); 
 $op = 1;
 for ($i = 1; $i <= $daysthismonth; $i++) {
  $possiblelunarday = $fisrtdaylunar + $op-1; 
  if ($possiblelunarday <= $everymonth[$lcj][$lci]) {
   $results[$i] = $mday[$possiblelunarday];
   $op += 1;
  }
  else {
   $results[$i] = $mday[1]; 
   $fisrtdaylunar = 1;
   $op = 2;
   $curmonthnum = ($everymonth[$lcj][0] != 0) ? 13 : 12;
   if ($lci + 1 > $curmonthnum) {
    $lci = 1;
    $lcj = $lcj + 1;
    $results['year'] .= '/' . $mten[$everymonth[$lcj][14]] . $mtwelve[$everymonth[$lcj][15]];
   }
   else {
    $lci = $lci + 1;
    $lcj = $lcj;
   }
  }
  if ($results[$i] == $mday[1]) {
   if ($everymonth[$lcj][0] != 0) {
    $monthss = ($lci > $everymonth[$lcj][0]) ? ($lci-1) : $lci;
    if ($lci == $everymonth[$lcj][0] + 1) {
     $monthssshow = $mmonth[0] . $mmonth[$monthss]; 
     $runyue = 1;
    }
    else {
     $monthssshow = $mmonth[$monthss];
    }
   }
   else {
    $monthss = $lci;
    $monthssshow = $mmonth[$monthss];
   }
   if ($monthss <= 10 && @$runyue != 1){
    $monthssshow .= $mmonth[13];
   }
   $results[$i] = $monthssshow;
  }
 }
 return $results;
}

// 农历用字
$lnlunarcalendar = array(
 'tiangan' => array("未知", "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"),
 'dizhi' => array("未知", "子年(鼠)", "丑年(牛)", "寅年(虎)", "卯年(兔)", "辰年(龙)",
  "巳年(蛇)", "午年(马)", "未年(羊)", "申年(猴)", "酉年(鸡)", "戌年(狗)", "亥年(猪)"),
 'month' => array("闰", "正", "二", "三", "四", "五", "六",
  "七", "八", "九", "十", "十一", "十二", "月"),
 'day' => array("未知", "初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
  "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
  "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十")
);

// 测试用例:2015年8月
print_r(lunarcalendar(8,2015));
?>

运行结果解析

调用 lunarcalendar(8, 2015) 返回数组中,关键信息包括:

  • [year] => 乙未年(羊): 表明2015年为乙未羊年。
  • [1] => 十七: 公历8月1日对应农历七月十七。
  • [14] => 七月: 公历8月14日是农历七月初一,因此显示月份名称而非日期数字。
  • [31] => 十八: 公历8月31日对应农历七月十八。

以上就是PHP实现的农历日历转换算法的详细内容,更多关于PHP农历计算的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表