Android日历阴阳历转换的实现(包括日期选择器)

公司出了一个类似生日管理的需求,日期选择器原本的项目里面就有,但是比较坑的是,阴阳历转换网上没找到好的办法,所以只能自己找资料自己写了。


在网上是看到一个阴阳历的算法,如下:


Android日历阴阳历转换的实现(包括日期选择器)

这张图解释得比较详细,但是有一个错误,第四个数E在图上转换为1111,实际应该是1110,刚开始拿过去验证1901年日历的时候,发现怎么也不对,差点放弃这个算法。还好最后检查出来了。这个算法之后验证是没有问题的。如有错误希望评论指出。


1901-2099对应的十六进制数组如下:


0x04AE53, 0x0A5748, 0x5526BD, 0x0D2650, 0x0D9544, 0x46AAB9, 0x056A4D, 0x09AD42,
0x24AEB6, 0x04AE4A,/*1901-1910*/
0x6A4DBE, 0x0A4D52, 0x0D2546, 0x5D52BA, 0x0B544E, 0x0D6A43, 0x296D37, 0x095B4B,
0x749BC1, 0x049754,/*1911-1920*/
0x0A4B48, 0x5B25BC, 0x06A550, 0x06D445, 0x4ADAB8, 0x02B64D, 0x095742, 0x2497B7,
0x04974A, 0x664B3E,/*1921-1930*/
0x0D4A51, 0x0EA546, 0x56D4BA, 0x05AD4E, 0x02B644, 0x393738, 0x092E4B, 0x7C96BF,
0x0C9553, 0x0D4A48,/*1931-1940*/
0x6DA53B, 0x0B554F, 0x056A45, 0x4AADB9, 0x025D4D, 0x092D42, 0x2C95B6, 0x0A954A,
0x7B4ABD, 0x06CA51,/*1941-1950*/
0x0B5546, 0x555ABB, 0x04DA4E, 0x0A5B43, 0x352BB8, 0x052B4C, 0x8A953F, 0x0E9552,
0x06AA48, 0x6AD53C,/*1951-1960*/
0x0AB54F, 0x04B645, 0x4A5739, 0x0A574D, 0x052642, 0x3E9335, 0x0D9549, 0x75AABE,
0x056A51, 0x096D46,/*1961-1970*/
0x54AEBB, 0x04AD4F, 0x0A4D43, 0x4D26B7, 0x0D254B, 0x8D52BF, 0x0B5452, 0x0B6A47,
0x696D3C, 0x095B50,/*1971-1980*/
0x049B45, 0x4A4BB9, 0x0A4B4D, 0xAB25C2, 0x06A554, 0x06D449, 0x6ADA3D, 0x0AB651,
0x093746, 0x5497BB,/*1981-1990*/
0x04974F, 0x064B44, 0x36A537, 0x0EA54A, 0x86B2BF, 0x05AC53, 0x0AB647, 0x5936BC,
0x092E50, 0x0C9645,/*1991-2000*/
0x4D4AB8, 0x0D4A4C, 0x0DA541, 0x25AAB6, 0x056A49, 0x7AADBD, 0x025D52, 0x092D47,
0x5C95BA, 0x0A954E,/*2001-2010*/
0x0B4A43, 0x4B5537, 0x0AD54A, 0x955ABF, 0x04BA53, 0x0A5B48, 0x652BBC, 0x052B50,
0x0A9345, 0x474AB9,/*2011-2020*/
0x06AA4C, 0x0AD541, 0x24DAB6, 0x04B64A, 0x69573D, 0x0A4E51, 0x0D2646, 0x5E933A,
0x0D534D, 0x05AA43,/*2021-2030*/
0x36B537, 0x096D4B, 0xB4AEBF, 0x04AD53, 0x0A4D48, 0x6D25BC, 0x0D254F, 0x0D5244,
0x5DAA38, 0x0B5A4C,/*2031-2040*/
0x056D41, 0x24ADB6, 0x049B4A, 0x7A4BBE, 0x0A4B51, 0x0AA546, 0x5B52BA, 0x06D24E,
0x0ADA42, 0x355B37,/*2041-2050*/
0x09374B, 0x8497C1, 0x049753, 0x064B48, 0x66A53C, 0x0EA54F, 0x06B244, 0x4AB638,
0x0AAE4C, 0x092E42,/*2051-2060*/
0x3C9735, 0x0C9649, 0x7D4ABD, 0x0D4A51, 0x0DA545, 0x55AABA, 0x056A4E, 0x0A6D43,
0x452EB7, 0x052D4B,/*2061-2070*/
0x8A95BF, 0x0A9553, 0x0B4A47, 0x6B553B, 0x0AD54F, 0x055A45, 0x4A5D38, 0x0A5B4C,
0x052B42, 0x3A93B6,/*2071-2080*/
0x069349, 0x7729BD, 0x06AA51, 0x0AD546, 0x54DABA, 0x04B64E, 0x0A5743, 0x452738,
0x0D264A, 0x8E933E,/*2081-2090*/
0x0D5252, 0x0DAA47, 0x66B53B, 0x056D4F, 0x04AE45, 0x4A4EB9, 0x0A4D4C, 0x0D1541,
0x2D92B5          /*2091-2099*/

利用此方法转化为二进制字符串:

/**
 * int 类型数据转成二进制的字符串,不足 int 类型位数时在前面添“0”以凑足位数
 */
public static String toFullBinaryString(int num) {
    char[] chs = new char[Integer.SIZE];
    for (int i = 0; i < Integer.SIZE; i++) {
        chs[Integer.SIZE - 1 - i] = (char) (((num >> i) & 1) + '0');
    }
    return new String(chs);
}

之后在此方法上解析农历,因为我要适配公司已有的选择器,所以入参和返回就按照自己计划的来:

/**
 * 解析农历年份
 *
 * @param year 为了跟picker匹配,多了一个将入参去掉""的操作
 * @return 返回结构为
 * map={year:{xxxx}
 *      month:{1,...,12}
 *      day:{
 *          {1,...,xx}
 *          ...
 *          {1,...,xx}
 *          }
 *     }
 */
public static HashMap<String, Object> parseLunarYear(String year) {
    HashMap<String, Object> map = new HashMap<>();
    List<String> monthList = new ArrayList<>();
    List<List<String>> dayList = new ArrayList<>();
    List<String> littleLunarList = new ArrayList<>();//农历的小月月份,29    List<String> largeLunarList = new ArrayList<>();//农历的大月月份,30    //把数组转化为list
    for (int i = 0; i < LUNAR_MONTHS_LITTLE.length; i++) {
        littleLunarList.add(i, LUNAR_MONTHS_LITTLE[i]);
    }
    for (int i = 0; i < LUNAR_MONTHS_LARGE.length; i++) {
        largeLunarList.add(i, LUNAR_MONTHS_LARGE[i]);
    }
    year = year.split("")[0];//因为传入的参数中包含了""这个字符,所以增加此操作
    int yearNum = Integer.parseInt(year);
    //只要后24位数
    String binaryYear = Utils.toFullBinaryString(LUNAR_YEARS[yearNum - 1901])
            .substring(8, 32);
    //闰月月份
    String leapMonth = binaryYear.substring(0, 4);
    int leapMonthNum = parseStr2Int(leapMonth);
    //每个月份对应的大小月,1是大月,0是小月
    String month = binaryYear.substring(4, 17);
    char[] months = month.toCharArray();
    for (int i = 0; i <= 11; i++) {
        monthList.add(i, LUNAR_MONTHS_NAME[i]);
        if (months[i] == '0') {
            dayList.add(i, littleLunarList);
        } else if (months[i] == '1') {
            dayList.add(i, largeLunarList);
        }
    }
    if (leapMonthNum > 0) {
        //如果有闰月,就需要考虑第十三个数,否则不需要考虑
        if (months[12] == '0') {
            dayList.add(12, littleLunarList);
        } else if (months[12] == '1') {
            dayList.add(12, largeLunarList);
        }
        monthList.add(leapMonthNum, "" + monthList.get(leapMonthNum - 1));
    }
    map.put("year", year);
    map.put("month", monthList);
    map.put("day", dayList);
    return map;
}

公历比较简单,就注意闰年平年就行了。在此附上代码:


/**
 * 解析公历年份
 *
 * @param year 为了跟picker匹配,多了一个将入参去掉""的操作
 * @return 返回结构为
 * map={year:{xxxx}
 *      month:{1,...,12}
 *      day:{
 *          {1,...,xx}
 *          ...
 *          {1,...,xx}
 *          }
 *      }
 */
public static HashMap<String, Object> parseAverageYear(String year) {
    HashMap<String, Object> map = new HashMap<>();
    year = year.split("")[0];
    int yearNum = Integer.parseInt(year);
    List<String> monthList = new ArrayList<>();
    List<List<String>> dayList = new ArrayList<>();
    List<String> secondMonthList = new ArrayList<>();//二月要拿出来区别对待
    List<String> littleMonthList = new ArrayList<>();//公历小月30    List<String> largeMonthList = new ArrayList<>();//公历大月30    if (yearNum % 4 == 0) {
        for (int i = 1; i < 30; i++) {
            secondMonthList.add(i + "");
        }
    } else {
        for (int i = 1; i < 29; i++) {
            secondMonthList.add(i + "");
        }
    }
    for (int i = 1; i < 31; i++) {
        littleMonthList.add(i + "");
        largeMonthList.add(i + "");
    }
    largeMonthList.add("31");
    for (int i = 1; i < 13; i++) {
        monthList.add(i + "");
        if (i == 2) {
            dayList.add(i - 1, secondMonthList);
        } else if (i == 1 || i == 3 || i == 5 || i == 7 || i == 8 || i == 10 
                || i == 12) {
            dayList.add(i - 1, largeMonthList);
        } else {
            dayList.add(i - 1, littleMonthList);
        }
    }
    map.put("year", year);
    map.put("month", monthList);
    map.put("day", dayList);
    return map;
}

接下来就是最重要的阴阳历转换了。代码我这边写起来可能有点复杂,但是实现思路比较简单,公历转农历的时候,用春节这天(也就是正月初一)做基准,计算要转换的公历日期离春节有多少天,在春节之后即本年,春节之前即减一年。农历转公历也同理,也用到了春节,另外也用到了元旦(公历一月一日),因为比较好计算。

代码如下:

/**
 * 将选中的公历日期,转化为农历日期,后两个入参皆为position,所以要从0开始计数
 *
 * @param year 为了跟picker匹配,多了一个将入参去掉""的操作
 * @return 返回结构为
 * map={year:{xxxx}
 *      month:{1,...,12}
 *      day:{
 *          {1,...,xx}
 *          ...
 *          {1,...,xx}
 *          }
 *      yearPosition:X
 *      monthPosition:X
 *      dayPosition:X
 *     }
 *
 *     yearPosition/monthPosition/dayPosition方便在日期选择器定位,平常使用可忽略这三个数据
 */
public static HashMap<String, Object> average2Lunar(String year, int monthPosition, int
        dayPosition) {
    HashMap<String, Object> lunarMap = parseLunarYear(year);
    year = year.split("")[0];
    int yearNum = Integer.parseInt(year);
    int[] monthMixAmount = yearNum%4 == 0?LEAP_MONTHS_MIX_AMOUNT : 
            AVERAGE_MONTHS_MIX_AMOUNT;
    String binaryYear = Utils.toFullBinaryString(LUNAR_YEARS[yearNum - 1901])
            .substring(8, 32);
    int springMonth = parseStr2Int(binaryYear.substring(17, 19));
    int springDay = parseStr2Int(binaryYear.substring(19, 24));
    if (monthPosition + 1 > springMonth || (monthPosition + 1 == springMonth 
            && dayPosition + 1 >= springDay)) {
        //春节和春节之后(年份不用变)
        List<List<String>> dayList = (List<List<String>>) lunarMap.get("day");
        int count = monthMixAmount[monthPosition] + dayPosition -
                (monthMixAmount[springMonth - 1] + springDay - 1);
        int tempCount = 0;
        for (int i = 0; i < dayList.size(); i++) {
            int tempCount2 = tempCount + dayList.get(i).size();
            if (tempCount2 > count) {
                lunarMap.put("yearPosition", yearNum - 1901);
                lunarMap.put("monthPosition", i);
                lunarMap.put("dayPosition", count - tempCount);
                break;
            } else {
                tempCount = tempCount2;
            }
        }
    } else {
        //春节之前(年份需要变)
        lunarMap = parseLunarYear(yearNum - 1 + "");
        List<List<String>> dayList = (List<List<String>>) lunarMap.get("day");
        int count = (monthMixAmount[springMonth - 1] + springDay - 1) -
                (monthMixAmount[monthPosition] + dayPosition);
        int tempCount = 0;
        for (int i = dayList.size() - 1; i >= 0; i--) {
            int tempCount2 = tempCount + dayList.get(i).size();
            if (tempCount2 >= count) {
                lunarMap.put("yearPosition", yearNum - 1 - 1901);
                lunarMap.put("monthPosition", i);
                lunarMap.put("dayPosition", dayList.get(i).size() - (count - tempCount));
                break;
            } else {
                tempCount = tempCount2;
            }
        }
    }
    return lunarMap;
}

/**
 * 将选中的农历日期,转化为公历日期,后两个入参皆为position,所以要从0开始计数
 *
 * @param year 为了跟picker匹配,多了一个将入参去掉""的操作
 * @return 返回结构为
 * map={year:{xxxx}
 *      month:{1,...,12}
 *      day:{
 *          {1,...,xx}
 *          ...
 *          {1,...,xx}
 *          }
 *      yearPosition:X
 *      monthPosition:X
 *      dayPosition:X
 *     }
 *
 *     yearPosition/monthPosition/dayPosition方便在日期选择器定位,平常使用可忽略这三个数据
 */
public static HashMap<String, Object> lunar2Average(String year, int monthPosition, int
        dayPosition) {
    HashMap<String, Object> lunarMap = parseLunarYear(year);
    year = year.split("")[0];
    int yearNum = Integer.parseInt(year);
    int[] monthMixAmount = yearNum%4 == 0?LEAP_MONTHS_MIX_AMOUNT :
            AVERAGE_MONTHS_MIX_AMOUNT;
    HashMap<String, Object> map = average2Lunar(yearNum + 1 + "", 0, 0);
    int newYearMonth = (int) map.get("monthPosition");//明年元旦对应今年的阴历月份
    int newYearDay = (int) map.get("dayPosition");//明年元旦对应今年的阴历日
    String binaryYear = Utils.toFullBinaryString(LUNAR_YEARS[yearNum - 1901])
            .substring(8, 32);
    int springMonth = parseStr2Int(binaryYear.substring(17, 19));
    int springDay = parseStr2Int(binaryYear.substring(19, 24));
    int springCount = (monthMixAmount[springMonth - 1] + springDay - 1);
    List<List<String>> dayList = (List<List<String>>) lunarMap.get("day");
    int count = 0;//叠加阴历月份
    int tempCount = 0;//记录需要转换的阴历月份离春节有多少天
    for (int i = 0; i < dayList.size(); i++) {
        if (i == monthPosition) {
            tempCount = count + dayPosition;
        }
        count = count + dayList.get(i).size();
    }
    int differ = springCount + tempCount;
    if (monthPosition < newYearMonth || (monthPosition == newYearMonth
            && dayPosition < newYearDay)) {
        //元旦之前
        // 记录当年元旦离选中的阴历有多少天,differ应小于365366
        for (int i = 0; i < monthMixAmount.length; i++) {
            if (monthMixAmount[i] > differ) {
                //第一次大于
                map.put("yearPosition", yearNum - 1901);
                map.put("monthPosition", i - 1);
                map.put("dayPosition", differ - monthMixAmount[i - 1]);
                break;
            } else if (differ == monthMixAmount[i]) {
                //如果等于,则是下个月一号
                map.put("yearPosition", yearNum - 1901);
                map.put("monthPosition", i);
                map.put("dayPosition", 0);
                break;
            } else if (differ > monthMixAmount[11]) {
                //如果differ超出前十一个月叠加的天数,则必为12                map.put("yearPosition", yearNum - 1901);
                map.put("monthPosition", 11);
                map.put("dayPosition", differ - monthMixAmount[11]);
                break;
            }
        }
    } else {
        //元旦之后,年份加1
        differ = differ - (yearNum % 4 == 0 ? 366 : 365);
        for (int i = 0; i < monthMixAmount.length; i++) {
            if (monthMixAmount[i] > differ) {
                //第一次大于
                map.put("yearPosition", yearNum - 1901 + 1);
                map.put("monthPosition", i - 1);
                map.put("dayPosition", differ - monthMixAmount[i - 1]);
                break;
            } else if (differ == monthMixAmount[i]) {
                //如果等于,则是下个月一号
                map.put("yearPosition", yearNum - 1901 + 1);
                map.put("monthPosition", i);
                map.put("dayPosition", 0);
                break;
            }
        }
    }
    return map;
}

至此,阴阳历解析和转换都完毕了。效果图:


Android日历阴阳历转换的实现(包括日期选择器)


Android日历阴阳历转换的实现(包括日期选择器)


Demo地址