본문 바로가기
프로젝트/인생캘린더

Flutter 음력변환 알고리즘 설명

by 반포한강공원 2026. 3. 19.

음력은 단순한 규칙으로 계산되지 않는다.

천문 현상을 기반으로 하기 때문에 예측공식이 없다.

 

태양력은 365.2422일, 음력월은 29.53095일.

12개월을 곱해보면, 1년이 354.367일이다.

태양력보다 11일이 부족하다.

 

1년이 지나면 음력설은 11일 앞당겨져 있다.

2년이 지나면 22일, 3년이 지나면 33일이 앞당겨져 있다.

그래서 3년마다 한번씩 윤달을 추가한다.

여전히 2~3일이 남으니 10년 정도 지나면 한 달을 추가해야 한다.

 

그런데 한달이 29일이니 차감공식이 조금 어렵다.

룰을 만들기엔 뭔가 딱 안 떨어지는 느낌.

 

특히 윤달을 넣는 기준이 애매하다.

24절기가 없는 달에 넣는다.

동지, 설날이 두 번일 수는 없으니 말이다.

 

그런데 그런 달이 2개 달일수도 있다.

그런 경우엔 앞 달에 윤달을 넣는다.

뭔가 복잡하다.

 

이걸 계산식으로 만들어서 실시간으로 불러 쓰기엔 불편하다.

그래서 미리 계산된 결과를 Array로 놓고 관리하는 걸 좋아한다.

필요할 땐 뒤에서 Batch를 돌려 데이터를 만들어내면 끝.

샘플코드

class LunarDate {
  final int year;
  final int month;
  final int day;
  final bool isLeapMonth;

  LunarDate({
    required this.year,
    required this.month,
    required this.day,
    required this.isLeapMonth,
  });

  @override
  String toString() {
    final leap = isLeapMonth ? '윤' : '';
    return '$year년 ${leap}${month}월 $day일';
  }
}

class SolarToLunarConverter {
  /// 1900-01-31 == 음력 1900-01-01 기준
  static final DateTime _minSolar = DateTime(1900, 1, 31);
  static final DateTime _maxSolar = DateTime(2099, 12, 31);

  /// 1900~2099 범위용 음력 데이터
  static const List<int> _lunarInfo = [
    0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0,
    0x09ad0, 0x055d2, 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540,
    0x0d6a0, 0x0ada2, 0x095b0, 0x14977, 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50,
    0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, 0x06566, 0x0d4a0,
    0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
    0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2,
    0x0a950, 0x0b557, 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5d0, 0x14573,
    0x052d0, 0x0a9a8, 0x0e950, 0x06aa0, 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4,
    0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, 0x096d0, 0x04dd5,
    0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b5a0, 0x195a6,
    0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46,
    0x0ab60, 0x09570, 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58,
    0x05ac0, 0x0ab60, 0x096d5, 0x092e0, 0x0c960, 0x0d954, 0x0d4a0, 0x0da50,
    0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, 0x0a950, 0x0b4a0,
    0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
    0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260,
    0x0ea65, 0x0d530, 0x05aa0, 0x076a3, 0x096d0, 0x04bd7, 0x04ad0, 0x0a4d0,
    0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, 0x0b5a0, 0x056d0, 0x055b2, 0x049b0,
    0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, 0x14b63, 0x09370,
    0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06aa0, 0x1a6c4, 0x0aae0,
    0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0,
    0x0a6d0, 0x055d4, 0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50,
    0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, 0x0b273, 0x06930, 0x07337, 0x06aa0,
    0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, 0x0e968, 0x0d520,
    0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,
    0x0d520,
  ];

  static int _leapMonth(int lunarYear) {
    return _lunarInfo[lunarYear - 1900] & 0xF;
  }

  static int _leapDays(int lunarYear) {
    if (_leapMonth(lunarYear) != 0) {
      return ((_lunarInfo[lunarYear - 1900] & 0x10000) != 0) ? 30 : 29;
    }
    return 0;
  }

  static int _monthDays(int lunarYear, int lunarMonth) {
    if (lunarMonth < 1 || lunarMonth > 12) {
      throw ArgumentError('lunarMonth must be in 1..12');
    }
    return ((_lunarInfo[lunarYear - 1900] & (0x10000 >> lunarMonth)) != 0)
        ? 30
        : 29;
  }

  static int _yearDays(int lunarYear) {
    int total = 348; // 29 * 12
    final info = _lunarInfo[lunarYear - 1900];
    int bit = 0x8000;

    for (int i = 0; i < 12; i++) {
      if ((info & bit) != 0) {
        total += 1;
      }
      bit >>= 1;
    }

    return total + _leapDays(lunarYear);
  }

  static LunarDate solarToLunar(int solarYear, int solarMonth, int solarDay) {
    final solar = DateTime(solarYear, solarMonth, solarDay);

    if (solar.isBefore(_minSolar) || solar.isAfter(_maxSolar)) {
      throw ArgumentError(
        '지원 범위는 ${_formatDate(_minSolar)} ~ ${_formatDate(_maxSolar)} 입니다.',
      );
    }

    int offset = solar.difference(_minSolar).inDays;

    int lunarYear = 1900;
    while (lunarYear < 2100) {
      final daysOfYear = _yearDays(lunarYear);
      if (offset < daysOfYear) {
        break;
      }
      offset -= daysOfYear;
      lunarYear += 1;
    }

    final leap = _leapMonth(lunarYear);
    bool isLeapMonth = false;
    int lunarMonth = 1;

    while (lunarMonth <= 12) {
      int daysOfMonth;

      if (leap != 0 && lunarMonth == leap + 1 && !isLeapMonth) {
        lunarMonth -= 1;
        isLeapMonth = true;
        daysOfMonth = _leapDays(lunarYear);
      } else {
        daysOfMonth = _monthDays(lunarYear, lunarMonth);
      }

      if (offset < daysOfMonth) {
        break;
      }

      offset -= daysOfMonth;

      if (isLeapMonth && lunarMonth == leap) {
        isLeapMonth = false;
      }

      lunarMonth += 1;
    }

    final lunarDay = offset + 1;

    return LunarDate(
      year: lunarYear,
      month: lunarMonth,
      day: lunarDay,
      isLeapMonth: isLeapMonth,
    );
  }

  static String _formatDate(DateTime d) {
    final y = d.year.toString().padLeft(4, '0');
    final m = d.month.toString().padLeft(2, '0');
    final day = d.day.toString().padLeft(2, '0');
    return '$y-$m-$day';
  }
}

 

사용예시

void main() {
  final lunar = SolarToLunarConverter.solarToLunar(2026, 3, 19);

  print(lunar.year);        // 2026
  print(lunar.month);       // 예: 2
  print(lunar.day);         // 예: 1
  print(lunar.isLeapMonth); // false
  print(lunar);             // 2026년 2월 1일
}

 

Flutter UI에서 쓰는 방법

final result = SolarToLunarConverter.solarToLunar(2026, 3, 19);
final text = result.toString();

 

 

"_lunarInfo" 에 대한 설명

- [ 윤달크기 ][ 월정보(12개월) ][ 윤달위치 ]

 

위치의미

하위 4비트 (0~3) 윤달 월 (0이면 없음)
0x10000 (16번째 비트) 윤달이 30일인지 여부
나머지 (12비트) 각 월이 29일/30일

 

 

참고. 24절기

24절기는 양력이다.

계절의 변화는 태양의 위치가 원인이기 때문이다.

 

조선시대에 24절기 계산이 어려웠던 이유는 관측을 할 수 없었기 때문이다.

태양을 쳐다보고 위치를 재야 했는데, 쳐다 볼 수가 없었던 것.

쉽게 쳐다볼 수 있는 달을 기준으로 달력을 만들었는데,

이게 해의 공전주기와 달라 천문관이 매번 계산을 새로 해야 했던 것.

 

지금은 양력을 그대로 쓰기 때문에 거의 변함이 없다.

다만, 양력도 1년이 365.2422일이기 때문에  4년에 한번씩 윤달을 만든다.

이 때문에 24절기 날짜가 하루씩 다를 때도 있다.

 

우리나라에서 쓰는 24절기는 기준점이 있는데, 중국의 화북지점이다.

황화강의 북쪽지점인데 중국에서 농경지가 가장 넓은 곳이다.

우리나라 전남 지역 정도와 위도가 일치해서,

우리나라의 기후가 24절기를 기준으로 며칠씩 늦는다.

 

하지만, 날짜는 그대로 쓴다.

씨 뿌리는 걸 조금 늦게 할 뿐.

 

세종대왕 때 우리만의 24절기를 만들려고 했다가, 실패했다는 건 영화에도 자주 나왔다.

지금은 천문현상을 많이 알기에 별차이가 없음을 인지하게 되었지만,

당시로선 하늘의 비밀을 푸는 거라, 정치적으로 엄청난 도전이었다.

 

끝.

 

반응형

댓글