鸿蒙万年历应用开发:CalendarEngine与农历算法实战
万年历是移动端高频应用,开发一款功能完整的鸿蒙万年历应用,不仅需要展示公历日期,还要集成农历、节气、传统节日等中国特色功能。本文基于HarmonyOS NEXT(API 20+),从零构建该应用,重点分享CalendarEngine日期计算引擎和农历转换算法的实现思路。项目采用MVVM架构,数据模型定义如下:DateInfo包含公历/农历年、月、日、星期、节气、节日等字段;ScheduleItem用于日程管理;FestivalInfo存储节日信息。
日历核心引擎CalendarEngine提供基础日期计算能力:
// CalendarEngine.ets
static DAYS_OF_MONTH: number[] = ;
static isLeapYear(year: number): boolean {
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
static getDaysOfMonth(year: number, month: number): number {
if (month === 2) return this.isLeapYear(year) ? 29 : 28;
return this.DAYS_OF_MONTH;
}
static getFirstDayOfMonth(year: number, month: number): number {
const m = month >= 3 ? month : month + 12;
const y = month >= 3 ? year : year - 1;
const k = y % 100;
const j = Math.floor(y / 100);
const day = 1;
const h = (day + Math.floor(13 * (m + 1) / 5) + k + Math.floor(k / 4) + Math.floor(j / 4) + 5 * j) % 7;
return (h + 6) % 7;
}
static getMonthGridRows(year: number, month: number): number {
const days = this.getDaysOfMonth(year, month);
const firstDay = this.getFirstDayOfMonth(year, month);
return Math.ceil((days + firstDay) / 7);
}
static formatDate(year: number, month: number, day: number, sep: string = '-'): string {
return `${year}${sep}${String(month).padStart(2,'0')}${sep}${String(day).padStart(2,'0')}`;
}
static parseDate(dateStr: string): { year: number; month: number; day: number } {
const parts = dateStr.split('-');
return { year: parseInt(parts), month: parseInt(parts), day: parseInt(parts) };
}
static daysBetween(date1: string, date2: string): number {
const d1 = this.parseDate(date1);
const d2 = this.parseDate(date2);
const toDays = (y: number, m: number, d: number): number => {
let days = 0;
for (let i = 1970; i < y; i++) days += this.isLeapYear(i) ? 366 : 365;
for (let i = 1; i < m; i++) days += this.getDaysOfMonth(y, i);
return days + d - 1;
};
return Math.abs(toDays(d1.year,d1.month,d1.day) - toDays(d2.year,d2.month,d2.day));
}
static getToday(): { year: number; month: number; day: number } {
const now = new Date();
return { year: now.getFullYear(), month: now.getMonth()+1, day: now.getDate() };
}
static getTodayString(): string {
const t = this.getToday();
return this.formatDate(t.year, t.month, t.day);
}
农历转换是核心难点。LunarDate类基于查表法和位运算实现公历转农历。核心思路:使用lunarInfo数组存储1900-2100年每年农历信息,每个元素的高4位表示闰月月份(0表示无闰月),低12位表示12个月的大小月(1=30天,0=29天),次高4位表示闰月天数。通过计算目标日期与1900年1月31日(农历1900年正月初一)的天数偏移,迭代确定农历年、月、日,并包含节气判断。
// LunarDate.ets
static LUNAR_MONTH_NAME: string[] = ['','正','二','三','四','五','六','七','八','九','十','冬','腊'];
static LUNAR_DAY_NAME: string[] = ['','初一','初二','初三','初四','初五','初六','初七','初八','初九','初十','十一','十二','十三','十四','十五','十六','十七','十八','十九','二十','廿一','廿二','廿三','廿四','廿五','廿六','廿七','廿八','廿九','三十'];
static SOLAR_TERM_NAME: string[] = ['小寒','大寒','立春','雨水','惊蛰','春分','清明','谷雨','立夏','小满','芒种','夏至','小暑','大暑','立秋','处暑','白露','秋分','寒露','霜降','立冬','小雪','大雪','冬至'];
// lunarInfo数组(完整数据详见原工程)
static LUNAR_INFO: number[] = ;
static toLunar(year: number, month: number, day: number): LunarInfo {
let offset = this.calculateOffset(year, month, day) - 30; // 减去基准日期1900-01-31对应天数
let lunarYear = 1900, lunarMonth = 1, lunarDay = 1, isLeapMonth = false;
while (lunarYear < 2100 && offset > 0) {
const daysInYear = this.getLunarYearDays(lunarYear);
if (offset < daysInYear) break;
offset -= daysInYear;
lunarYear++;
}
const leapMonth = this.getLeapMonth(lunarYear);
const monthDays = this.getLunarYearMonths(lunarYear);
let monthIndex = 1;
while (monthIndex <= 12 && offset > 0) {
if (leapMonth > 0 && monthIndex === leapMonth + 1 && !isLeapMonth) {
isLeapMonth = true;
monthIndex--;
const leapDays = this.getLeapMonthDays(lunarYear);
if (offset < leapDays) break;
offset -= leapDays;
} else {
if (isLeapMonth && monthIndex === leapMonth + 1) isLeapMonth = false;
const normalDays = monthDays;
if (offset < normalDays) break;
offset -= normalDays;
}
monthIndex++;
}
lunarMonth = isLeapMonth ? monthIndex - 1 : monthIndex;
lunarDay = offset + 1;
const solarTerm = this.getSolarTerm(year, month, day);
const festivals = this.getFestivals(year, month, day, lunarMonth, lunarDay, isLeapMonth);
return { lunarYear, lunarMonth, lunarDay, isLeapMonth, lunarMonthName: this.getLunarMonthName(lunarMonth, isLeapMonth), lunarDayName: this.LUNAR_DAY_NAME||'', solarTerm, festivals };
}
private static calculateOffset(year: number, month: number, day: number): number {
let days = 0;
for (let y = 1900; y < year; y++) days += CalendarEngine.isLeapYear(y) ? 366 : 365;
for (let m = 1; m < month; m++) days += CalendarEngine.getDaysOfMonth(year, m);
days += day;
return days;
}
private static getLunarYearDays(year: number): number {
const idx = year - 1900;
if (idx < 0 || idx >= this.LUNAR_INFO.length) return 365;
let days = 0;
const info = this.LUNAR_INFO;
for (let i = 0; i < 12; i++) days += (info & (1 << i)) ? 30 : 29;
return days + this.getLeapMonthDays(year);
}
private static getLeapMonth(year: number): number {
const idx = year - 1900;
if (idx < 0 || idx >= this.LUNAR_INFO.length) return 0;
return (this.LUNAR_INFO >> 12) & 0x0f;
}
private static getLeapMonthDays(year: number): number {
const leapMonth = this.getLeapMonth(year);
if (leapMonth === 0) return 0;
const idx = year - 1900;
return (this.LUNAR_INFO >> 13) & 1 ? 30 : 29;
}
private static getSolarTerm(year: number, month: number, day: number): string {
// 简化实现:使用SOLAR_TERM_OFFSET表判断日期是否接近标准节气
const index = (month - 1) * 2;
const termDay1 = this.SOLAR_TERM_OFFSET;
const termDay2 = this.SOLAR_TERM_OFFSET;
if (day >= termDay1 - 2 && day <= termDay1 + 2) return this.SOLAR_TERM_NAME;
if (day >= termDay2 - 2 && day <= termDay2 + 2) return this.SOLAR_TERM_NAME;
return '';
}
在UI层面,利用ArkUI的Grid组件构建月视图,通过CalendarEngine获取每月第一天星期和总天数动态生成网格,每个格子显示公历日期,下方小字显示农历或节气。使用@State和@Link管理选中日期和视图刷新。结合CalendarPicker组件可快速实现日期选择。需要注意的是,HarmonyOS NEXT的Stage模型要求严格声明权限,但万年历应用通常无需特殊权限。日程管理可基于关系型数据库(RDB Store)或首选项(Preferences)实现本地存储,具体可根据数据量选择。
总结:通过CalendarEngine提供的基础日期能力加上LunarDate的农历查表算法,开发者可以快速在鸿蒙应用中集成完整的农历、节气功能。该方案性能稳定,代码复杂度可控,适合直接嵌入生产级应用。后续可继续扩展节日库、倒计时、主题换肤等能力。
Re: 鸿蒙万年历应用开发:CalendarEngine与农历算法实战
楼主分享得很详细,尤其是CalendarEngine里getFirstDayOfMonth用的蔡勒公式变体,以及农历查表法+位运算存储闰月和大小的思路,实际开发中非常实用。想请教一下,lunarInfo数组里节气数据是单独存储的还是和农历信息整合在一起计算的?另外,对于1900年之前或2100年之后的日期,楼主有没有考虑做外推处理,还是直接限制范围?UI层这块,月视图和年视图切换时,按楼主MVVM的结构,数据绑定是直接用@State监听DateInfo的变化吗?Re: 鸿蒙万年历应用开发:CalendarEngine与农历算法实战
看到这篇分享感觉很实用,特别是CalendarEngine里面那几个静态方法,代码写得很干净,getFirstDayOfMonth用的蔡勒公式变体,日期推算效率应该不错。农历那部分查表法加位运算的思路也很清晰,把闰月和大月小月压缩到一个int里存,既节省空间又方便移位解析,是个经典的嵌入式优化写法。 不过有个小建议:农历表里有没有考虑2100年以后的扩展?如果要做成通用模块,可能得预留一个动态表加载接口。另外,节气判断那部分楼主只提了“包含”,没贴具体代码,很期待后续能补充二分法或者定气法的实现细节。 整体来说,这个帖子的代码质量和讲解节奏都很舒服,适合拿来当鸿蒙应用入门日历开发的模板。Re: 鸿蒙万年历应用开发:CalendarEngine与农历算法实战
楼主分享得很详细,代码都贴出来了,鸿蒙的CalendarEngine这套工具方法写得挺规整,尤其是getFirstDayOfMonth用的蔡勒公式变体,直接算出每月第一天是星期几,做日历网格排版很方便。 农历转换这块用查表法和位运算来压缩存储1900到2100年的数据,既节省空间又高效,确实是鸿蒙端做万年历比较实用的做法。想请教一下,节气判断部分用的是固定表法还是基于太阳黄经的动态计算?如果结合HarmonyOS NEXT的本地化能力,将来直接调用系统农历接口会不会更省心?感谢分享!
页:
[1]