PHP 实现农历日历转换算法详解
本文展示了一种基于预置数据表的 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 表:
- 外层循环
$lcj逐年增加,内层循环$lci逐月累加天数$mtotal。 - 当
$mtotal >= $total时,说明已跨越目标日期,跳出循环。
此时:
- $lcj 指向目标年份在表中的索引。
- $lci 指向目标月份在表中的索引。
- $fisrtdaylunar 通过计算当月首日与目标日的农历日期偏移量得出:$everymonth[$lcj][$lci] - ($mtotal - $total)。
第4步:生成当月每日的农历结果
遍历目标公历月的每一天(1 到 daysthismonth):
- 日期转换:根据
$fisrtdaylunar递增,利用$lnlunarcalendar['day']映射出“初一”、“初二”等。 - 月份更替判断:当农历日期超过当前月最大天数时,重置为初一,并进入下一个月。
- 闰年/跨年处理:
- 检查当前年是否有闰月(
$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农历计算的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









