南漂鱼
奋斗路上的南漂鱼

写在前面,本文章是对JS日期转农历的一点记录,如有错误或者更好见解欢迎指出和讨论

发布 2021-03-06
最新修正 2021-03-12(考虑闰月存在闰正月和闰腊月更新部分算法和注释)
最新修正 2021-08-20(修改初始化年月日参数名写错)

这里先简单介绍农历表中十六进制规则方便后面转换的了解

农历数据和节气数据没有固定的日期一般是由天文台观测数据得来,例如:紫金山天文台.香港天文台
举个例子1949年农历对应十六进制0x0b557
高位 0000 1011 0101 0101 0111 低位

0000 1011 0101 0101 0111
20-17 16-13 12-9 8-5 4-1
高位[20-17]代表闰月大小,0001代表大闰月30天,0000代表小闰月29天(当且低位存在闰月有效)
中间位[16-5]代表一月到十二月大小月,其中1代表大月30天,0代表小月29天 (注:其中一月对应16位,十二月对应5位)
低位[4-1]代表闰月所在的月份,上述例子7代表的就是闰月在七月,若无闰月就为0

整理农历对应天数表格

1月 2月 3月 4月 5月 6月 7月 润7月 8月 9月 10月 11月 12月
30 29 30 30 29 30 29 29 30 29 30 29 30
// 农历查询表
const lunarYearArr = new Array(
  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, 0x055c0, 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
);

计算农历和部分基础方法

// 格式化日期
const week = new Array("星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六");
const formatDate = function(time, type){
  const date = new Date(time);
  const y = date.getFullYear();
  const m = date.getMonth()+1<10 ? "0"+(date.getMonth()+1) : date.getMonth()+1;
  const d = date.getDate()<10 ? "0"+date.getDate() : date.getDate();
  const w = date.getDay();
  const wStr = week[w];
  if(type){
    return {
      year: y,
      month: m,
      day: d,
      week: w,
      weekStr: wStr
    };
  }
  else{
    return y + "-" + m + "-" + d;
  }
}
// 基准年
const BENCHMARK_YEAR = 1900;
// 基准时间戳(国际零时区毫秒)
const BENCHMARK_TIME = Date.UTC(BENCHMARK_YEAR, 0, 30);
/**
 * 计算农历年是否有闰月, 参数为存储农历年的16进制
 * @param {Number} ly
 * @return {Boolean}
 * @eg {Function} hasLeapMonth(0x0b557)
 */
const hasLeapMonth = function(ly){
  //农历低四位不等于0即为存在闰月
  //存在闰月即返回闰月所在月份
  if(ly & 0x0000f){
    return ly & 0x0000f
  }
  else{
    return false
  }
}
/**
 * 计算农历闰月天数, 参数为存储农历年的16进制
 * @param {Number} ly
 * @return {Number}
 * @eg {Function} leapMonthDays(0x0b557)
 */
const leapMonthDays = function(ly){
  //农历高四位等于0即为闰小月29天, 不等于0(等于1)即为闰大月30天
  //存在闰月即返回闰月天数
  if(hasLeapMonth(ly)){
    return (ly & 0xf0000) ? 30 : 29
  }
  else{
    return 0
  }
}
/**
 * 计算农历一年的总天数, 参数为存储农历年的16进制
 * @param {Number} ly
 * @return {Number}
 * @eg {Function} lunarYearDays(0x0b557)
 */
const lunarYearDays = function(ly){
  //从高位第16位(1月)起向右移至低位第5位(12月)累加天数
  let totalDays = 0;
  for(let i=0x8000; i>0x8; i>>=1){
    let monthDays = (ly & i) ? 30 : 29;
    totalDays += monthDays;
  }
  //考虑是否有闰月天数
  if(hasLeapMonth(ly)){
    totalDays += leapMonthDays(ly);
  }
  return totalDays
}
/**
 * 计算农历每个月的天数, 参数为存储农历年的16进制
 * @param {Number} ly
 * @return {Array}
 * @eg {Function} lunarYearMonths(0x0b557)
 */
const lunarYearMonths = function(ly){
  //从高位第16位(1月)起向右移至低位第5位(12月)添加数组每项
  let monthArr = [];
  for(let i=0x8000; i>0x8; i>>=1){
    monthArr.push((ly & i) ? 30 : 29);
  }
  //考虑是否有闰月天数
  if(hasLeapMonth(ly)){
    monthArr.splice(hasLeapMonth(ly), 0, leapMonthDays(ly));
  }
  return monthArr
}
// 将农历年转换为天干, 参数为存储农历年的16进制
const getTianGan = function(ly){
  let tianGanKey = (ly - 3) % 10;
  if(tianGanKey === 0) tianGanKey = 10;
  return tianGan[tianGanKey - 1]
}

// 将农历年转换为地支, 参数为存储农历年的16进制
const getDiZhi = function(ly){
  let diZhiKey = (ly - 3) % 12;
  if(diZhiKey === 0) diZhiKey = 12;
  return diZhi[diZhiKey - 1]
}

实现方法入口

/**
 * 主要实现方法入口, 参数为字符串格式日期或者时间戳(毫秒)
 * @param {String | Number} date
 * @return {Object}
 * @eg {Function} sloarToLunar("1949-01-29") || sloarToLunar(660268800000)
 */

// 天干
const tianGan = new Array("甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸");
// 地支
const diZhi = new Array("子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥");
// 农历月
const lunarMonth = new Array("正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊");
// 农历日
const lunarDay = new Array("一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "初", "廿");

const sloarToLunar = function(date){
  const time = formatDate(date, true);
  // 初始化农历年月日
  let ly, lm, ld;
  // 初始化传入年月日
  let sy = time.year;
  let sm = time.month;
  let sd = time.day;
  if(sy <= BENCHMARK_YEAR || sy >= 2100) return {code: 400, msg: "输入年限不在查询表范围", status: false};
  // 计算与基准相差天数
  let differenceDay = (Date.UTC(Number(sy), Number(sm), Number(sd)) - BENCHMARK_TIME) / 86400000; //24*60*60*1000;

  //计算农历年份
  for(let y=0; y<lunarYearArr.length; y++){
    differenceDay -= lunarYearDays(lunarYearArr[y]);
    if(differenceDay <= 0){
      ly = BENCHMARK_YEAR + y;
      // 计算返回农历年份确定后的剩余天数(用于计算农历月)
      differenceDay += lunarYearDays(lunarYearArr[y]);
      break;
    }
  }

  //计算农历月份
  for(let m = 0; m<lunarYearMonths(lunarYearArr[ly - BENCHMARK_YEAR]).length; m++){
    differenceDay -= lunarYearMonths(lunarYearArr[ly - BENCHMARK_YEAR])[m];
    if(differenceDay <= 0){
      // 有闰月时, 月份的数组长度会变成13, 因此, 当闰月月份小于等于m时, lm不需要加1
      if(hasLeapMonth(lunarYearArr[ly - BENCHMARK_YEAR]) && hasLeapMonth(lunarYearArr[ly - BENCHMARK_YEAR]) <= m){
        if(hasLeapMonth(lunarYearArr[ly - BENCHMARK_YEAR]) < m){
          lm = m;
        }
        else if(hasLeapMonth(lunarYearArr[ly - BENCHMARK_YEAR]) === m){
          lm = "闰" + m;
        }
        else{
          lm = m + 1;
        }
      }
      else{
        lm = m + 1;
      }
      // 获取农历月份确定后的剩余天数(用于计算农历日)
      differenceDay += lunarYearMonths(lunarYearArr[ly - BENCHMARK_YEAR])[m];
      break;
    }
  }

  //计算农历日
  ld = differenceDay;
  
    // 将计算出来的农历月份转换成汉字月份, 闰月需要在前面加上闰字
  if(hasLeapMonth(lunarYearArr[ly - BENCHMARK_YEAR]) && (typeof (lm) === "string" && lm.indexOf("闰") > -1)){
    lm = `闰${lunarMonth[/\d/.exec(lm) - 1]}月`;
  }
  else{
    lm = `${lunarMonth[lm - 1]}月`;
  }

  // 将计算出来的农历年份转换为天干地支年
  ly = getTianGan(ly) + getDiZhi(ly);

  // 将计算出来的农历月份转换成汉字月份, 闰月需要在前面加上闰字
  if(hasLeapMonth(lunarYearArr[ly - BENCHMARK_YEAR]) && (typeof (lm) === "string" && lm.indexOf("闰") > -1)){
    lm = `闰${lunarMonth[/\d/.exec(lm) - 1]}月`;
  }
  else{
    lm = `${lunarMonth[lm - 1]}月`;
  }

  // 将计算出来的农历天数转换成汉字
  if(ld < 11){
    ld = `${lunarDay[10]}${lunarDay[ld-1]}`;
  }
  else if(ld > 10 && ld < 20){
    ld = `${lunarDay[9]}${lunarDay[ld-11]}`;
  }
  else if (ld === 20){
    ld = `${lunarDay[1]}${lunarDay[9]}`;
  }
  else if(ld > 20 && ld < 30){
    ld = `${lunarDay[11]}${lunarDay[ld-21]}`;
  }
  else if(ld === 30){
    ld = `${lunarDay[2]}${lunarDay[9]}`;
  }

  // 后面更多返回可调用计算方法输出, 注意调用的时间农历年月日使用格式化中文前的数据
  return {
    code: 200,
    status: true,
    sy: sy, //传入年
    sm: sm, //传入月
    sd: sd, //传入日
    ly: ly, //农历年
    lm: lm, //农历月
    ld: ld, //农历日
  }
}

获取生肖方法(以下未定义参数名对应入口方法里的参数)

// 初始化生肖数组
const zodiac = new Array("鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪");
// 计算生肖(仅做粗略计算, 精确计算请按农历节气, 农历算法由农历正月初一开始 和 干支纪年法由节气立春开始)
let zodiacStr = zodiac[(ly-BENCHMARK_YEAR) % 12]; //ly为初始化农历年

获取农历节日方法(以下未定义参数名对应入口方法里的参数)

// 初始化农历节日数组
const lunarFestival = new Array(
  "0101 春节",
  "0107 人胜节",
  "0115 元宵节",
  "0120 天穿节",
  "0125 天仓节",
  "0201 中和节",
  "0202 春龙节(龙抬头)",
  "0212 花朝节",
  "0303 上巳节",
  "0408 浴佛节",
  "0505 端午节",
  "0606 晒衣节",
  "0707 乞巧节(七夕)",
  "0715 中元节",
  "0801 天医节",
  "0815 中秋节",
  "0909 重阳节",
  "1001 寒衣节",
  "1015 下元节",
  "1208 腊八节",
  "1224 祭灶节(小年)"
);
// 计算农历节日
// ly lm ld 为农历年月日
let lunar = null;
lunarFestival.forEach((item, index)=>{
  let str = item.split(" ");
  if(str[0] == `${lm<10 ? "0"+lm : lm}${ld<10 ? "0"+ld : ld}`){
    lunar = str[1];
  }
  //部分节日需要手动计算例如春节前一天除夕,清明前一天寒食节等等
  //考虑到闰正月和闰腊月计算相对复杂这里仅做粗略计算,严谨请参照(平气法已过时)定气法具体百度搜索相关资料做调整
  let lymArr = lunarYearMonths(lunarYearArr[ly - BENCHMARK_YEAR]);
  if(`${lm}${ld}` == `12${lymArr[lymArr.length-1]}`){
    lunar = "除夕";
  }
})

获取公历节日方法(以下未定义参数名对应入口方法里的参数)

// 初始化公历节日数组(更多公历节日可自行在数组内添加相应的日期和名称即可)
const gregorianFestival = new Array(
  "0101 元旦",
  "0214 情人节",
  "0307 女生节",
  "0308 妇女节",
  "0312 植树节",
  "0314 白色情人节",
  "0315 消费者权益日",
  "0401 愚人节",
  "0404 复活节",
  "0501 劳动节",
  "0504 青年节",
  "0510 母亲节",
  "0512 护士节",
  "0601 儿童节",
  "0620 父亲节",
  "0701 建党节",
  "0801 建军节",
  "0910 教师节",
  "0928 孔子诞辰",
  "1001 国庆节",
  "1006 老人节",
  "1024 联合国日",
  "1101 万圣节",
  "1125 感恩节",
  "1224 平安夜",
  "1225 圣诞节"
);
// 计算公历节日
// sy sm sd 为传入年月日
let gregorian = null;
gregorianFestival.forEach((item, index)=>{
  let str = item.split(" ");
  if(str[0] == `${sm}${sd}`){
    gregorian = str[1];
  }
})

获取节气方法(以下未定义参数名对应入口方法里的参数)

网上有通式寿星公式 [Y×D+C]-L (其中Y=年代数、D=0.2422、L=闰年数、C取决于节气和年份)这方法简单大家也可以尝试

下面平年周期算法注释会简单解析各数据若想了解更详细节气的内容可以到末尾文章文献 [6] [7]

// 初始化节气数组
// 按地球每转过15°为一节气,因为地球轨道是椭圆所以会存在计算误差,数组里数据对应各个节气距离一年起点的毫秒数
// 31556925974.7为地球公转周期天数换算的毫秒数(约365.2422天)
// Date.UTC(1900,0,6,2,5)为日期1900-01-06 02:05毫秒数
const solarTermInfo = new Array(
  0,1272480000,2548020000,3830160000,5120220000,6420840000,
  7732020000,9055260000,10388940000,11733060000,13084320000,14441580000,
  15800580000,17159340000,18513780000,19861980000,21201000000,22529640000,
  23846820000,25152600000,26447700000,27733440000,29011920000,30285480000
);
const solarTerm = new Array(
  "小寒","大寒","立春","雨水","惊蛰","春分",
  "清明","谷雨","立夏","小满","芒种","夏至",
  "小暑","大暑","立秋","处暑","白露","秋分",
  "寒露","霜降","立冬","小雪","大雪","冬至"
);
// 计算节气
// sy sm sd 为传入年月日
const getSolarTerm = function(sy, sm, sd){
  sm -= 1;
  let solarTermStr = "";
  //月份乘2是因为每月平均2节气对应二十四节气加一考虑存在闰月
  let tmp1 = new Date((31556925974.7*(sy-1900)+solarTermInfo[sm*2+1]) + Date.UTC(1900,0,6,2,5));
  let tmp2 = tmp1.getUTCDate();
  if(tmp2 == sd) solarTermStr = solarTerm[sm*2+1];
  tmp1 = new Date((31556925974.7*(sy-1900)+solarTermInfo[sm*2]) + Date.UTC(1900,0,6,2,5));
  tmp2 = tmp1.getUTCDate();
  if(tmp2 == sd) solarTermStr = solarTerm[sm*2];
  if(sd > 1){
    sd -= 1;
  }
  else{
    sm -= 1;
    sd = 31;
    if(sm < 0){
      sy -= 1;
      sm = 11;
    }
  }
  return solarTermStr;
}

获取星座方法(以下未定义参数名对应入口方法里的参数)

// 初始化星座数组
const constellation = new Array(
  {s: "0120", e: "0218", c: "水瓶座"},
  {s: "0219", e: "0320", c: "双鱼座"},
  {s: "0321", e: "0419", c: "白羊座"},
  {s: "0420", e: "0520", c: "金牛座"},
  {s: "0521", e: "0621", c: "双子座"},
  {s: "0622", e: "0722", c: "巨蟹座"},
  {s: "0723", e: "0822", c: "狮子座"},
  {s: "0823", e: "0922", c: "处女座"},
  {s: "0923", e: "1023", c: "天秤座"},
  {s: "1024", e: "1122", c: "天蝎座"},
  {s: "1123", e: "1221", c: "射手座"},
  {s: "1222", e: "0119", c: "摩羯座"}
);
// 计算星座
// sy sm sd 为传入年月日
let constellationStr = null;
let c = sm * 100 + sd; //把传入日期数据转化为与数组中可对比的数字
constellation.forEach((item, index)=>{
  if(!constellationStr){
    if(c >= Number(item.s) && c <= Number(item.e)){
      constellationStr = item.c;
    }
    else if(c >= Number(item.s) || c <= Number(item.e)){
      constellationStr = item.c;
    }
  }
})

劳动成果实不容易如需要转载或引用请注明出处即可 谢谢!

参考文献
[1] 第一星座网
[2] 天气万年历
[3] 关于日历实现代码里的0x04bd8, 0x04ae0, 0x0a570的说明
[4] 原生js实现公历转农历
[5] 1900年至2100年公历、农历互转Js代码
[6] 二十四节气计算公式 [Y×D+C]-L中的C是怎么规定的
[7] 二十四节气

posted on 2021-03-06 14:43  南漂鱼  阅读(4851)  评论(7)    收藏  举报