diff --git a/docs/amlich-known-issues.md b/docs/amlich-known-issues.md
index c1e80e1..0697a15 100644
--- a/docs/amlich-known-issues.md
+++ b/docs/amlich-known-issues.md
@@ -35,8 +35,11 @@ Both dates are pinned in `lunar_test.go` `knownDates` so a future
**Razor-edge lunations from 2072 on.** New moon within ~2 minutes of UTC+7
midnight, so the month boundary — and therefore output within roughly 30 days
of it — may differ by one day from future official tables: 09/12/2072,
-15/11/2077, 07/05/2130, 26/05/2150, 17/05/2159, 22/01/2175, 26/01/2199. Not
-fixable today, only documentable. The bot gives no caveat for these dates.
+15/11/2077, 07/05/2130, 26/05/2150, 17/05/2159, 22/01/2175, 26/01/2199. Those
+are the disputed new-moon days themselves; the engine begins each of these
+months on the following day (`disputedMonthStarts` in `lunar.go`). Not fixable
+today. Both commands append a caveat when the result falls in a lunar month
+that starts or ends on one of these boundaries.
**ΔT extrapolation drift.** The two-branch polynomial diverges from actual
Earth rotation (ΔT has been roughly flat near 69 s since ~2016 against
@@ -64,7 +67,9 @@ source will see a mismatch that is not an error.
**`/duonglich` defaults inside leap months.** A bare day argument fills in the
current month as a regular month; the leap month requires the explicit `nhuan`
flag. In leap years such as 2028 (leap 5) and 2031 (leap 3) this is ambiguous
-to the user, but the output is correct for what was entered.
+to the user, but the output is correct for what was entered. When the queried
+month is also that year's leap month (and the exact leap date exists), the
+reply appends a hint suggesting the `nhuan` flag.
## Handled, with regression tests
@@ -77,14 +82,20 @@ to the user, but the output is correct for what was entered.
## Open questions
-1. Should the seven razor-edge lunations from 2072 on carry a caveat in the
- bot's reply? Currently silent.
-2. Should pre-1968 UTC+8 be modelled explicitly? The current algorithm matches
- the published record on both known razor-edge cases by numeric coincidence
- rather than by rule. An explicit UTC+8 mode would change roughly 1/24 of
- pre-1968 month boundaries and diverge from the standard everyone else uses —
- not worth doing unless a user reports a concrete mismatch.
+1. ~~Should the seven razor-edge lunations carry a caveat?~~ Resolved: both
+ commands append a caveat for results in the lunar months touching those
+ boundaries — months only, not the whole lunar year.
+2. ~~Should pre-1968 UTC+8 be modelled explicitly?~~ Closed: don't build. Per
+ Hồ Ngọc Đức's historic-calendar notes it is not even a single rule — the
+ North used UTC+8 from 1945–67, but the South used UTC+7 until 1959 and
+ UTC+8 only from 1960–67, with dynastic tables before 1945. Any concrete
+ user-reported mismatch gets a doc note, not a mode.
3. Revisit the ΔT model if the flat IERS trend persists. A data-driven update
- only pays off near razor-edge boundaries.
+ only pays off near razor-edge boundaries — and would diverge from the
+ reference algorithm the whole ecosystem runs, so it needs official tables
+ as cover before it is worth it.
4. Watch for state calendar bureau tables published past 2100; they would be
- the first ground truth for the 2072+ disputes.
+ the first ground truth for the 2072+ disputes. Related far-future
+ uncertainty in the same bucket: the CGPM votes in late 2026 on abolishing
+ the leap second, which would let civil UTC+7 drift slowly from the
+ astronomical time the algorithm models.
diff --git a/internal/modules/amlich/golden_test.go b/internal/modules/amlich/golden_test.go
new file mode 100644
index 0000000..aafeea1
--- /dev/null
+++ b/internal/modules/amlich/golden_test.go
@@ -0,0 +1,105 @@
+package amlich
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "testing"
+)
+
+var update = flag.Bool("update", false, "regenerate golden files")
+
+// goldenTable renders every lunar year in the supported range as one line:
+// "YYYY L n1 n2 ... nN" — L is the leap month number (0 = none) and n* are the
+// month lengths in chronological order starting at tháng 1, with the leap
+// month in sequence (12 entries for a normal year, 13 for a leap year).
+func goldenTable(t *testing.T) string {
+ t.Helper()
+ var b strings.Builder
+ for year := minLunarYear; year <= maxLunarYear; year++ {
+ type monthStart struct {
+ jd, month int
+ leap bool
+ }
+ var starts []monthStart
+ for m := 1; m <= 12; m++ {
+ dd, mm, yy, err := lunarToSolar(1, m, year, false)
+ if err != nil {
+ t.Fatalf("lunarToSolar(1, %d, %d): %v", m, year, err)
+ }
+ starts = append(starts, monthStart{jdFromDate(dd, mm, yy), m, false})
+ if dd, mm, yy, err := lunarToSolar(1, m, year, true); err == nil {
+ starts = append(starts, monthStart{jdFromDate(dd, mm, yy), m, true})
+ }
+ }
+ sort.Slice(starts, func(i, j int) bool { return starts[i].jd < starts[j].jd })
+ // The last month of year Y ends where tháng 1 of Y+1 begins. For
+ // maxLunarYear this evaluates the engine one year past the supported
+ // range, which the astronomy handles fine — only the bot commands
+ // enforce the bounds.
+ dd, mm, yy, err := lunarToSolar(1, 1, year+1, false)
+ if err != nil {
+ t.Fatalf("lunarToSolar(1, 1, %d): %v", year+1, err)
+ }
+ nextYearStart := jdFromDate(dd, mm, yy)
+ leapMonth := 0
+ for _, s := range starts {
+ if s.leap {
+ leapMonth = s.month
+ }
+ }
+ if want := 12; (leapMonth == 0 && len(starts) != want) || (leapMonth != 0 && len(starts) != want+1) {
+ t.Fatalf("year %d: %d month starts with leap month %d", year, len(starts), leapMonth)
+ }
+ fmt.Fprintf(&b, "%d %d", year, leapMonth)
+ for i, s := range starts {
+ end := nextYearStart
+ if i+1 < len(starts) {
+ end = starts[i+1].jd
+ }
+ if length := end - s.jd; length != 29 && length != 30 {
+ t.Fatalf("year %d month %d (leap=%v): length %d days", year, s.month, s.leap, length)
+ } else {
+ fmt.Fprintf(&b, " %d", length)
+ }
+ }
+ b.WriteByte('\n')
+ }
+ return b.String()
+}
+
+// TestGoldenTable compares the engine's full 1800–2199 month structure against
+// the committed golden file. The round-trip test proves the two conversion
+// directions agree with each other; this pins the verified boundary placement
+// itself, so a future engine change cannot shift months while staying
+// self-consistent. Regenerate deliberately with:
+//
+// go test ./internal/modules/amlich/ -run TestGoldenTable -update
+func TestGoldenTable(t *testing.T) {
+ got := goldenTable(t)
+ golden := filepath.Join("testdata", "lunar-years-1800-2199.txt")
+ if *update {
+ if err := os.WriteFile(golden, []byte(got), 0o644); err != nil {
+ t.Fatalf("write golden file: %v", err)
+ }
+ return
+ }
+ want, err := os.ReadFile(golden)
+ if err != nil {
+ t.Fatalf("read golden file (regenerate with -update): %v", err)
+ }
+ if got == string(want) {
+ return
+ }
+ gotLines := strings.Split(got, "\n")
+ wantLines := strings.Split(string(want), "\n")
+ for i := 0; i < len(gotLines) && i < len(wantLines); i++ {
+ if gotLines[i] != wantLines[i] {
+ t.Fatalf("golden mismatch at line %d:\n got: %s\nwant: %s", i+1, gotLines[i], wantLines[i])
+ }
+ }
+ t.Fatalf("golden mismatch: got %d lines, want %d", len(gotLines), len(wantLines))
+}
diff --git a/internal/modules/amlich/handlers.go b/internal/modules/amlich/handlers.go
index 8eeb33a..94cdec5 100644
--- a/internal/modules/amlich/handlers.go
+++ b/internal/modules/amlich/handlers.go
@@ -27,6 +27,15 @@ const duonglichUsage = `Cách dùng: /duonglich
[nhuan] — đ
// next to the algorithm it protects.
var yearRangeMessage = fmt.Sprintf("Chỉ hỗ trợ các năm từ %d đến %d.", minLunarYear, maxLunarYear)
+// leapHintFormat is appended to a /duonglich reply when the queried month is
+// also that year's leap month and the user gave no "nhuan" flag — the input
+// was ambiguous, and the answer shown is for the regular month.
+const leapHintFormat = `Lưu ý: năm âm lịch %d có tháng %d nhuận — thêm "nhuan" nếu ý bạn là tháng nhuận.`
+
+// disputedCaveat is appended when the result falls in a lunar month whose
+// boundary is astronomically too close to call (see disputedMonthStarts).
+const disputedCaveat = "Lưu ý: ngày này gần ranh giới tháng âm lịch chưa chắc chắn; kết quả có thể lệch 1 ngày so với lịch chính thức sau này."
+
func amlichCommand() modules.Command {
return modules.Command{
Name: "amlich",
@@ -55,6 +64,9 @@ func amlichCommand() modules.Command {
lunarDay, lunarMonth, lunarYear, leap := solarToLunar(day, month, year)
text := fmt.Sprintf("Dương lịch %02d/%02d/%d là ngày %d tháng %d%s năm %s %d âm lịch.",
day, month, year, lunarDay, lunarMonth, leapLabel(leap), canChiYear(lunarYear), lunarYear)
+ if nearDisputedBoundary(jdFromDate(day, month, year)) {
+ text += "\n" + disputedCaveat
+ }
return chathelper.Reply(ctx, b, update.Message, text)
},
}
@@ -102,6 +114,17 @@ func duonglichCommand() modules.Command {
}
text := fmt.Sprintf("Âm lịch ngày %d tháng %d%s năm %s %d là %02d/%02d/%d dương lịch.",
day, month, leapLabel(leap), canChiYear(year), year, solarDay, solarMonth, solarYear)
+ // A bare month is ambiguous when the year also has that month as a
+ // leap month and the exact leap date exists; probing lunarToSolar
+ // reuses its validation (a 29-day leap month can't hide a day 30).
+ if !leap {
+ if _, _, _, hintErr := lunarToSolar(day, month, year, true); hintErr == nil {
+ text += "\n" + fmt.Sprintf(leapHintFormat, year, month)
+ }
+ }
+ if nearDisputedBoundary(jdFromDate(solarDay, solarMonth, solarYear)) {
+ text += "\n" + disputedCaveat
+ }
return chathelper.Reply(ctx, b, update.Message, text)
},
}
diff --git a/internal/modules/amlich/handlers_test.go b/internal/modules/amlich/handlers_test.go
index 1335900..b4d6d55 100644
--- a/internal/modules/amlich/handlers_test.go
+++ b/internal/modules/amlich/handlers_test.go
@@ -202,6 +202,56 @@ func TestDuonglich_RejectsWrongLeapMonth(t *testing.T) {
}
}
+// A bare month that is also the year's leap month is ambiguous input; the
+// reply must carry the disambiguation hint — and only then.
+func TestDuonglich_LeapMonthHint(t *testing.T) {
+ solarDay, solarMonth, solarYear, err := lunarToSolar(5, 5, 2028, false)
+ if err != nil {
+ t.Fatalf("lunarToSolar(5/5/2028): %v", err)
+ }
+ want := fmt.Sprintf("Âm lịch ngày 5 tháng 5 năm Mậu Thân 2028 là %02d/%02d/%d dương lịch.\n", solarDay, solarMonth, solarYear) +
+ `Lưu ý: năm âm lịch 2028 có tháng 5 nhuận — thêm "nhuan" nếu ý bạn là tháng nhuận.`
+ if got := replyFor(t, "/duonglich 5/5/2028"); got != want {
+ t.Errorf("reply = %q, want %q", got, want)
+ }
+}
+
+func TestDuonglich_NoHintWhenNhuanExplicit(t *testing.T) {
+ if got := replyFor(t, "/duonglich 5/5/2028 nhuan"); strings.Contains(got, "Lưu ý") {
+ t.Errorf("explicit nhuan reply = %q, want no hint", got)
+ }
+}
+
+func TestDuonglich_NoHintWithoutLeapMonth(t *testing.T) {
+ if got := replyFor(t, "/duonglich 5/5/2027"); strings.Contains(got, "Lưu ý") {
+ t.Errorf("non-leap-year reply = %q, want no hint", got)
+ }
+}
+
+// Dates inside a lunar month touching a disputed boundary (see
+// disputedMonthStarts) must carry the caveat; nearby ordinary dates must not.
+// Absence on modern dates is also pinned by the exact-string assertions in
+// TestAmlich_ConvertsKnownDate and TestDuonglich_ConvertsKnownDate.
+func TestAmlich_DisputedBoundaryCaveat(t *testing.T) {
+ if got := replyFor(t, "/amlich 20/12/2072"); !strings.HasSuffix(got, "\n"+disputedCaveat) {
+ t.Errorf("reply = %q, want disputed-boundary caveat", got)
+ }
+ if got := replyFor(t, "/amlich 01/06/2072"); strings.Contains(got, "Lưu ý") {
+ t.Errorf("reply = %q, want no caveat", got)
+ }
+}
+
+func TestDuonglich_DisputedBoundaryCaveat(t *testing.T) {
+ lunarDay, lunarMonth, lunarYear, leap := solarToLunar(20, 12, 2072)
+ cmd := fmt.Sprintf("/duonglich %d/%d/%d", lunarDay, lunarMonth, lunarYear)
+ if leap {
+ cmd += " nhuan"
+ }
+ if got := replyFor(t, cmd); !strings.HasSuffix(got, "\n"+disputedCaveat) {
+ t.Errorf("reply for %q = %q, want disputed-boundary caveat", cmd, got)
+ }
+}
+
func TestDuonglich_InvalidInputRepliesUsage(t *testing.T) {
for _, text := range []string{
"/duonglich",
diff --git a/internal/modules/amlich/lunar.go b/internal/modules/amlich/lunar.go
index bc59ed6..166f4a4 100644
--- a/internal/modules/amlich/lunar.go
+++ b/internal/modules/amlich/lunar.go
@@ -251,6 +251,36 @@ func lunarToSolar(day, month, year int, leap bool) (dd, mm, yy int, err error) {
return dd, mm, yy, nil
}
+// disputedMonthStarts holds the Julian day numbers of the lunar-month starts
+// whose defining new moon falls within ~2 minutes of UTC+7 midnight, where
+// ΔT uncertainty exceeds the margin — the true boundary may sit one day
+// earlier in future official tables (see docs/amlich-known-issues.md, which
+// lists the disputed new-moon days themselves; this engine begins each of
+// these months on the following day). Conversions touching these months carry
+// a caveat in the bot's reply.
+var disputedMonthStarts = map[int]bool{
+ jdFromDate(10, 12, 2072): true,
+ jdFromDate(16, 11, 2077): true,
+ jdFromDate(8, 5, 2130): true,
+ jdFromDate(27, 5, 2150): true,
+ jdFromDate(18, 5, 2159): true,
+ jdFromDate(23, 1, 2175): true,
+ jdFromDate(27, 1, 2199): true,
+}
+
+// nearDisputedBoundary reports whether the lunar month containing the solar
+// day jdn starts or ends on a disputed boundary — i.e. whether the conversion
+// result for that day could shift by one day against future official tables.
+func nearDisputedBoundary(jdn int) bool {
+ k := floorInt((float64(jdn) - jdNewMoonEpoch) / newMoonCycle)
+ monthStart := getNewMoonDay(k + 1)
+ for monthStart > jdn {
+ k--
+ monthStart = getNewMoonDay(k + 1)
+ }
+ return disputedMonthStarts[monthStart] || disputedMonthStarts[getNewMoonDay(k+2)]
+}
+
// canNames and chiNames are the sexagesimal-cycle stems and branches used to
// name lunar years (Giáp Thìn, Ất Tỵ, …).
var canNames = [...]string{"Giáp", "Ất", "Bính", "Đinh", "Mậu", "Kỷ", "Canh", "Tân", "Nhâm", "Quý"}
diff --git a/internal/modules/amlich/lunar_test.go b/internal/modules/amlich/lunar_test.go
index 9953742..2d3d4d3 100644
--- a/internal/modules/amlich/lunar_test.go
+++ b/internal/modules/amlich/lunar_test.go
@@ -203,6 +203,44 @@ func TestLeapMonthTable(t *testing.T) {
}
}
+// TestDisputedMonthStarts_AreMonthStarts verifies every entry in the disputed
+// set is an actual lunar-month start per the engine — a mistyped or off-by-one
+// date would silently flag the wrong month.
+func TestDisputedMonthStarts_AreMonthStarts(t *testing.T) {
+ if len(disputedMonthStarts) != 7 {
+ t.Fatalf("disputed set has %d entries, want 7", len(disputedMonthStarts))
+ }
+ for jd := range disputedMonthStarts {
+ dd, mm, yy := jdToDate(jd)
+ day, _, _, _ := solarToLunar(dd, mm, yy)
+ if day != 1 {
+ t.Errorf("%02d/%02d/%d (jd %d) is lunar day %d, want month start (day 1)", dd, mm, yy, jd, day)
+ }
+ }
+}
+
+// TestNearDisputedBoundary covers the month starting on a disputed boundary,
+// the month ending on one, and ordinary dates on both sides.
+func TestNearDisputedBoundary(t *testing.T) {
+ cases := []struct {
+ dd, mm, yy int
+ want bool
+ }{
+ {10, 12, 2072, true}, // first day of the disputed month
+ {20, 12, 2072, true}, // inside the month starting on the disputed boundary
+ {9, 12, 2072, true}, // last day of the month ending on the disputed boundary
+ {1, 12, 2072, true}, // inside the month ending on the disputed boundary
+ {1, 6, 2072, false}, // same year, far from the boundary
+ {1, 2, 2073, false}, // after the disputed month
+ {29, 1, 2025, false}, // ordinary modern date
+ }
+ for _, tc := range cases {
+ if got := nearDisputedBoundary(jdFromDate(tc.dd, tc.mm, tc.yy)); got != tc.want {
+ t.Errorf("nearDisputedBoundary(%02d/%02d/%d) = %v, want %v", tc.dd, tc.mm, tc.yy, got, tc.want)
+ }
+ }
+}
+
func TestCanChiYear(t *testing.T) {
for year, want := range map[int]string{
2024: "Giáp Thìn",
diff --git a/internal/modules/amlich/testdata/lunar-years-1800-2199.txt b/internal/modules/amlich/testdata/lunar-years-1800-2199.txt
new file mode 100644
index 0000000..bcfc88f
--- /dev/null
+++ b/internal/modules/amlich/testdata/lunar-years-1800-2199.txt
@@ -0,0 +1,400 @@
+1800 4 30 29 30 29 30 30 29 30 29 30 29 29 30
+1801 0 29 30 30 29 30 29 30 30 29 30 29 30
+1802 0 29 29 30 29 30 29 30 30 29 30 30 29
+1803 2 30 29 29 30 29 30 29 30 30 29 30 30 29
+1804 0 30 29 29 30 29 29 30 30 29 30 30 30
+1805 7 29 30 29 29 30 29 29 30 29 30 30 30 29
+1806 0 30 30 29 29 30 29 29 30 29 30 30 29
+1807 0 30 30 30 29 29 30 29 29 30 29 30 29
+1808 5 30 30 30 29 30 29 30 29 29 30 29 30 29
+1809 0 30 30 29 30 30 29 30 29 30 29 29 30
+1810 0 29 30 29 30 30 29 30 29 30 30 29 30
+1811 3 29 29 30 29 30 29 30 30 29 30 30 29 30
+1812 0 29 29 30 29 30 29 30 29 30 30 30 29
+1813 0 30 29 29 30 29 29 30 29 30 30 30 29
+1814 2 30 30 29 29 30 29 29 30 29 30 30 29 30
+1815 0 30 30 29 29 30 29 29 30 29 30 29 30
+1816 6 30 30 29 30 29 30 29 29 30 29 30 29 30
+1817 0 30 29 30 30 29 30 29 29 30 29 30 29
+1818 0 30 29 30 30 29 30 30 29 30 29 29 30
+1819 4 29 30 29 30 29 30 30 29 30 30 29 30 29
+1820 0 29 30 29 30 29 30 29 30 30 29 30 30
+1821 0 29 29 30 29 29 30 29 30 30 30 29 30
+1822 3 30 29 29 30 29 29 30 29 30 30 29 30 30
+1823 0 30 29 29 30 29 29 30 29 30 29 30 30
+1824 7 30 29 30 29 30 29 29 30 29 30 29 30 30
+1825 0 29 30 30 29 30 29 29 30 29 30 29 30
+1826 0 29 30 30 30 29 30 29 29 30 29 30 29
+1827 5 30 29 30 30 29 30 29 30 29 30 29 30 29
+1828 0 30 29 30 29 30 29 30 30 29 30 29 30
+1829 0 29 30 29 30 29 30 29 30 30 29 30 30
+1830 4 29 29 30 29 29 30 29 30 30 29 30 30 30
+1831 0 29 29 30 29 29 30 29 30 29 30 30 30
+1832 9 29 30 29 30 29 29 30 29 30 29 30 30 30
+1833 0 29 30 29 30 29 29 30 29 30 29 30 30
+1834 0 29 30 30 29 30 29 29 30 29 30 29 30
+1835 6 29 30 30 29 30 30 29 29 30 29 30 29 30
+1836 0 29 30 29 30 30 29 30 29 30 29 30 29
+1837 0 30 29 30 29 30 29 30 30 29 30 29 30
+1838 4 29 30 29 29 30 29 30 30 29 30 30 29 30
+1839 0 29 30 29 29 30 29 30 29 30 30 30 29
+1840 0 30 29 30 29 29 30 29 30 29 30 30 30
+1841 3 29 30 29 30 29 29 30 29 29 30 30 29 30
+1842 0 30 30 29 30 29 29 30 29 29 30 30 29
+1843 7 30 30 29 30 30 29 29 30 29 30 29 30 29
+1844 0 30 29 30 30 29 30 29 30 29 30 29 30
+1845 0 29 30 29 30 29 30 30 29 30 29 30 29
+1846 5 30 29 29 30 30 29 30 29 30 30 29 30 29
+1847 0 30 29 29 30 29 30 29 30 30 30 29 30
+1848 0 29 30 29 29 30 29 30 29 30 30 29 30
+1849 4 30 29 30 29 29 30 29 29 30 30 29 30 30
+1850 0 30 29 30 29 29 30 29 29 30 30 29 30
+1851 8 30 30 29 30 29 29 30 29 29 30 29 30 30
+1852 0 30 29 30 29 30 29 30 29 29 30 29 30
+1853 0 30 29 30 30 29 30 29 30 29 30 29 30
+1854 7 29 29 30 30 29 30 30 29 30 29 30 29 30
+1855 0 29 29 30 29 30 30 29 30 30 29 30 29
+1856 0 30 29 29 30 29 30 29 30 30 29 30 30
+1857 5 29 30 29 29 30 29 29 30 30 29 30 30 30
+1858 0 29 30 29 29 30 29 29 30 29 30 30 30
+1859 0 30 29 30 29 29 30 29 29 30 29 30 30
+1860 3 30 29 30 30 29 29 30 29 29 30 29 30 30
+1861 0 29 30 30 29 30 29 30 29 29 30 29 30
+1862 8 29 30 30 29 30 30 29 30 29 30 29 29 30
+1863 0 29 30 29 30 30 29 30 30 29 30 29 30
+1864 0 29 29 30 29 30 29 30 30 29 30 30 29
+1865 5 30 29 29 30 29 30 29 30 29 30 30 30 29
+1866 0 30 29 29 30 29 29 30 29 30 30 30 30
+1867 0 29 30 29 29 30 29 29 30 29 30 30 30
+1868 4 29 30 30 29 29 30 29 29 30 29 30 30 29
+1869 0 30 30 29 30 29 30 29 29 30 29 30 29
+1870 10 30 30 30 29 30 29 30 29 29 30 29 30 29
+1871 0 30 30 29 30 30 29 30 29 30 29 29 30
+1872 0 29 30 29 30 30 29 30 29 30 30 29 30
+1873 6 29 29 30 29 30 29 30 30 29 30 30 29 30
+1874 0 29 29 30 29 29 30 30 29 30 30 30 29
+1875 0 30 29 29 30 29 29 30 29 30 30 30 29
+1876 5 30 30 29 29 30 29 29 30 29 30 30 29 30
+1877 0 30 30 29 29 30 29 29 30 29 30 29 30
+1878 0 30 30 29 30 29 30 29 29 30 29 30 29
+1879 3 30 30 29 30 30 29 30 29 29 30 29 30 29
+1880 0 30 29 30 30 29 30 29 30 29 30 29 30
+1881 7 29 30 29 30 29 30 30 29 30 29 30 29 30
+1882 0 29 30 29 30 29 30 29 30 30 29 30 30
+1883 0 29 29 30 29 29 30 29 30 30 30 29 30
+1884 5 30 29 29 30 29 29 30 29 30 30 29 30 30
+1885 0 30 29 29 30 29 29 30 29 30 29 30 30
+1886 0 30 29 30 29 30 29 29 30 29 30 29 30
+1887 4 30 29 30 30 29 30 29 29 30 29 30 29 30
+1888 0 29 30 30 29 30 30 29 29 30 29 30 29
+1889 0 30 29 30 30 29 30 29 30 29 30 29 30
+1890 2 29 30 29 30 29 30 29 30 30 29 30 29 30
+1891 0 29 30 29 29 30 30 29 30 30 29 30 29
+1892 6 30 29 30 29 29 30 29 30 30 29 30 30 29
+1893 0 30 29 30 29 29 30 29 30 29 30 30 30
+1894 0 29 30 29 30 29 29 30 29 30 29 30 30
+1895 5 29 30 30 29 30 29 29 30 29 30 29 30 29
+1896 0 30 30 30 29 30 29 29 30 29 30 29 30
+1897 0 29 30 30 29 30 29 30 29 30 29 30 29
+1898 3 30 29 30 29 30 30 29 30 29 30 29 30 29
+1899 0 30 29 30 29 30 29 30 30 29 30 29 30
+1900 8 29 30 29 29 30 29 30 30 29 30 30 29 30
+1901 0 29 30 29 29 30 29 30 29 30 30 30 29
+1902 0 30 29 30 29 29 30 29 30 29 30 30 29
+1903 5 30 30 29 30 29 29 30 29 29 30 30 29 30
+1904 0 30 30 29 30 29 29 30 29 29 30 30 29
+1905 0 30 30 29 30 30 29 29 30 29 29 30 30
+1906 4 29 30 29 30 30 29 30 29 30 29 30 29 30
+1907 0 29 30 29 30 29 30 30 29 30 29 30 29
+1908 0 30 29 29 30 29 30 30 29 30 30 29 30
+1909 2 29 30 29 29 30 29 30 29 30 30 30 29 30
+1910 0 29 30 29 29 30 29 30 29 30 30 29 30
+1911 6 30 29 30 29 29 30 29 29 30 30 29 30 30
+1912 0 30 29 30 29 29 30 29 29 30 30 29 30
+1913 0 30 30 29 30 29 29 30 29 29 30 29 30
+1914 5 30 30 29 30 29 30 29 30 29 29 30 29 30
+1915 0 30 29 30 30 29 30 29 30 29 30 29 29
+1916 0 30 29 30 30 29 30 30 29 30 29 30 29
+1917 3 30 29 29 30 29 30 30 29 30 30 29 30 29
+1918 0 30 29 29 30 29 30 29 30 30 29 30 30
+1919 7 29 30 29 29 30 29 29 30 30 29 30 30 30
+1920 0 29 30 29 29 30 29 29 30 29 30 30 30
+1921 0 30 29 30 29 29 30 29 29 30 29 30 30
+1922 6 30 29 30 30 29 29 30 29 29 30 29 30 30
+1923 0 29 30 30 29 30 29 30 29 29 30 29 30
+1924 0 29 30 30 29 30 30 29 30 29 30 29 29
+1925 4 30 29 30 29 30 30 29 30 30 29 30 29 30
+1926 0 29 29 30 29 30 29 30 30 29 30 30 29
+1927 0 30 29 29 30 29 30 29 30 29 30 30 30
+1928 2 29 30 29 29 30 29 29 30 29 30 30 30 30
+1929 0 29 30 29 29 30 29 29 30 29 30 30 30
+1930 6 29 30 30 29 29 30 29 29 30 29 30 30 29
+1931 0 30 30 29 30 29 30 29 29 30 29 30 29
+1932 0 30 30 30 29 30 29 30 29 29 30 29 30
+1933 5 29 30 30 29 30 29 30 30 29 29 30 29 30
+1934 0 29 30 29 30 30 29 30 29 30 30 29 29
+1935 0 30 29 30 29 30 29 30 30 29 30 30 29
+1936 3 30 29 29 30 29 29 30 30 29 30 30 29 30
+1937 0 30 29 29 30 29 29 30 29 30 30 30 29
+1938 8 30 30 29 29 30 29 29 30 29 30 30 29 30
+1939 0 30 29 30 29 30 29 29 30 29 30 29 30
+1940 0 30 30 29 30 29 30 29 29 30 29 30 29
+1941 6 30 30 29 30 30 29 30 29 29 30 29 30 29
+1942 0 30 29 30 30 29 30 29 30 29 30 29 30
+1943 0 29 30 29 30 29 30 30 29 30 29 30 29
+1944 4 30 29 30 29 30 29 30 29 30 30 29 30 30
+1945 0 29 29 30 29 29 30 29 30 30 30 29 30
+1946 0 30 29 29 30 29 29 30 29 30 30 29 30
+1947 2 30 29 30 29 30 29 29 30 29 30 29 30 30
+1948 0 30 29 30 29 30 29 29 30 29 30 29 30
+1949 7 30 29 30 30 29 30 29 29 30 29 30 29 30
+1950 0 29 30 30 29 30 29 30 29 30 29 30 29
+1951 0 30 29 30 29 30 30 29 30 29 30 29 30
+1952 5 29 30 29 30 29 30 29 30 30 29 30 29 30
+1953 0 29 30 29 29 30 29 30 30 30 29 30 29
+1954 0 30 29 30 29 29 30 29 30 30 29 30 30
+1955 3 29 30 29 30 29 29 30 29 30 29 30 30 30
+1956 0 29 30 29 30 29 29 30 29 29 30 30 30
+1957 8 29 30 30 29 30 29 29 30 29 29 30 30 29
+1958 0 30 30 30 29 30 29 29 30 29 30 29 30
+1959 0 29 30 30 29 30 29 30 29 30 29 30 29
+1960 6 30 29 30 29 30 30 29 30 29 30 29 30 29
+1961 0 30 29 29 30 30 29 30 30 29 30 29 30
+1962 0 29 30 29 29 30 29 30 30 29 30 30 29
+1963 4 30 29 30 29 29 30 29 30 29 30 30 30 29
+1964 0 30 29 30 29 29 30 29 29 30 30 30 29
+1965 0 30 30 29 30 29 29 30 29 29 30 30 29
+1966 3 30 30 30 29 30 29 29 30 29 29 30 30 29
+1967 0 30 30 29 30 30 29 29 30 29 29 30 29
+1968 7 30 30 29 30 30 29 30 29 30 29 30 29 29
+1969 0 30 30 29 30 29 30 30 29 30 29 30 29
+1970 0 30 29 29 30 29 30 30 29 30 30 29 30
+1971 5 29 30 29 29 30 29 30 29 30 30 30 29 30
+1972 0 29 30 29 29 30 29 30 29 30 30 29 30
+1973 0 30 29 30 29 29 30 29 29 30 30 29 30
+1974 4 30 30 29 30 29 29 30 29 29 30 29 30 30
+1975 0 30 29 30 30 29 29 30 29 29 30 29 30
+1976 8 30 30 29 30 29 30 29 30 29 29 30 29 30
+1977 0 30 29 30 30 29 30 29 30 29 30 29 29
+1978 0 30 29 30 30 29 30 29 30 30 29 30 29
+1979 6 29 30 29 30 29 30 30 29 30 30 29 30 29
+1980 0 30 29 29 30 29 30 29 30 30 29 30 30
+1981 0 29 30 29 29 30 29 29 30 30 29 30 30
+1982 4 30 29 30 29 29 30 29 29 30 29 30 30 30
+1983 0 30 29 30 29 29 30 29 29 30 29 30 30
+1984 0 30 29 30 29 30 29 30 29 29 30 29 30
+1985 2 30 29 30 30 29 30 29 30 29 29 30 29 30
+1986 0 29 30 30 29 30 30 29 30 29 29 30 29
+1987 7 30 29 30 29 30 30 29 30 30 29 30 29 29
+1988 0 30 29 30 29 30 29 30 30 29 30 30 29
+1989 0 30 29 29 30 29 29 30 30 29 30 30 30
+1990 5 29 30 29 29 30 29 29 30 29 30 30 30 30
+1991 0 29 30 29 29 30 29 29 30 29 30 30 30
+1992 0 29 30 30 29 29 30 29 29 30 29 30 30
+1993 3 29 30 30 29 30 29 30 29 29 30 29 30 29
+1994 0 30 30 30 29 30 29 30 29 29 30 29 30
+1995 8 29 30 30 29 30 29 30 29 30 29 30 29 30
+1996 0 29 30 29 30 29 30 30 29 30 29 30 29
+1997 0 30 29 30 29 30 29 30 29 30 30 29 30
+1998 5 30 29 29 30 29 29 30 30 29 30 30 29 30
+1999 0 30 29 29 30 29 29 30 29 30 30 30 29
+2000 0 30 30 29 29 30 29 29 30 29 30 30 29
+2001 4 30 30 29 30 29 30 29 29 30 29 30 29 30
+2002 0 30 30 29 30 29 30 29 29 30 29 30 29
+2003 0 30 30 29 30 30 29 30 29 29 30 29 30
+2004 2 29 30 29 30 30 29 30 29 30 29 30 29 30
+2005 0 29 30 29 30 29 30 30 29 30 29 30 29
+2006 7 30 29 30 29 29 30 30 29 30 30 29 30 29
+2007 0 30 29 30 29 29 30 29 30 30 30 29 30
+2008 0 30 29 29 30 29 29 30 29 30 29 30 30
+2009 5 30 29 30 29 30 29 29 30 29 30 29 30 30
+2010 0 30 29 30 29 30 29 29 30 29 30 29 30
+2011 0 30 29 30 30 29 30 29 29 30 29 30 29
+2012 4 30 29 30 30 29 30 29 30 29 30 29 30 29
+2013 0 30 29 30 29 30 30 29 30 29 30 29 30
+2014 9 29 30 29 30 29 30 29 30 30 29 30 29 30
+2015 0 29 30 29 29 30 29 30 30 30 29 30 29
+2016 0 30 29 30 29 29 30 29 30 30 29 30 30
+2017 6 29 30 29 30 29 29 30 29 30 29 30 30 30
+2018 0 29 30 29 30 29 29 30 29 29 30 30 30
+2019 0 29 30 30 29 30 29 29 30 29 29 30 30
+2020 4 29 30 30 30 29 30 29 29 30 29 29 30 30
+2021 0 29 30 30 29 30 29 30 29 30 29 30 29
+2022 0 30 29 30 29 30 30 29 30 29 30 29 30
+2023 2 29 30 29 29 30 30 29 30 30 29 30 29 30
+2024 0 29 30 29 29 30 29 30 30 29 30 30 29
+2025 6 30 29 30 29 29 30 29 30 29 30 30 30 29
+2026 0 30 29 30 29 29 30 29 29 30 30 30 29
+2027 0 30 30 29 30 29 29 30 29 29 30 30 29
+2028 5 30 30 30 29 30 29 29 30 29 29 30 30 29
+2029 0 30 30 29 30 29 30 29 30 29 29 30 29
+2030 0 30 30 29 30 30 29 30 29 30 29 30 29
+2031 3 29 30 29 30 30 29 30 30 29 30 29 30 29
+2032 0 29 30 29 30 29 30 30 29 30 30 29 30
+2033 11 29 30 29 29 30 29 30 29 30 30 30 29 30
+2034 0 29 30 29 29 30 29 29 30 30 30 29 30
+2035 0 30 29 30 29 29 30 29 29 30 30 29 30
+2036 6 30 30 29 30 29 29 30 29 29 30 29 30 30
+2037 0 30 29 30 30 29 29 30 29 29 30 29 30
+2038 0 30 29 30 30 29 30 29 30 29 29 30 29
+2039 5 30 30 29 30 30 29 30 29 30 29 29 30 29
+2040 0 30 29 30 30 29 30 29 30 30 29 30 29
+2041 0 29 30 29 30 29 30 29 30 30 30 29 30
+2042 2 29 30 29 29 30 29 30 29 30 30 29 30 30
+2043 0 29 30 29 29 30 29 29 30 30 29 30 30
+2044 7 30 29 30 29 29 30 29 29 30 29 30 30 30
+2045 0 30 29 30 29 29 30 29 29 30 29 30 30
+2046 0 30 29 30 29 30 29 30 29 29 30 29 30
+2047 5 30 29 30 30 29 30 29 30 29 29 30 29 30
+2048 0 29 30 30 29 30 30 29 30 29 29 30 29
+2049 0 30 29 30 29 30 30 29 30 29 30 30 29
+2050 3 29 30 29 30 29 30 29 30 30 29 30 30 29
+2051 0 29 30 29 30 29 29 30 30 29 30 30 30
+2052 8 29 30 29 29 30 29 29 30 29 30 30 30 29
+2053 0 30 30 29 29 30 29 29 30 29 30 30 30
+2054 0 29 30 30 29 29 30 29 29 30 29 30 30
+2055 6 29 30 30 29 30 29 30 29 29 30 29 30 29
+2056 0 30 30 29 30 30 29 30 29 29 30 29 30
+2057 0 29 30 29 30 30 29 30 29 30 29 30 29
+2058 4 30 29 30 29 30 29 30 30 29 30 29 30 29
+2059 0 30 29 30 29 30 29 30 29 30 30 29 30
+2060 0 29 30 29 30 29 29 30 29 30 30 30 29
+2061 3 30 30 29 29 30 29 29 30 29 30 30 30 29
+2062 0 30 30 29 29 30 29 29 30 29 30 29 30
+2063 7 30 30 29 30 29 30 29 29 30 29 30 29 30
+2064 0 30 30 29 30 29 30 29 29 30 29 30 29
+2065 0 30 30 29 30 30 29 29 30 29 30 29 30
+2066 5 29 30 29 30 30 29 30 29 30 29 30 29 30
+2067 0 29 30 29 30 29 30 30 29 30 29 30 29
+2068 0 30 29 30 29 29 30 30 29 30 30 29 30
+2069 4 29 30 29 30 29 29 30 29 30 30 30 29 30
+2070 0 29 30 29 30 29 29 30 29 30 29 30 30
+2071 8 30 29 30 29 30 29 29 30 29 30 29 30 30
+2072 0 30 29 30 29 30 29 29 30 29 30 29 30
+2073 0 30 29 30 30 29 30 29 29 30 29 29 30
+2074 6 30 29 30 30 29 30 29 30 29 30 29 30 29
+2075 0 30 29 30 29 30 30 29 30 29 30 29 30
+2076 0 29 30 29 30 29 30 29 30 30 29 30 29
+2077 4 30 29 30 29 29 30 29 30 30 30 29 30 29
+2078 0 30 29 30 29 29 30 29 30 29 30 30 30
+2079 0 29 30 29 30 29 29 30 29 30 29 30 30
+2080 3 30 29 30 29 30 29 29 29 30 29 30 30 30
+2081 0 29 30 30 29 30 29 29 30 29 29 30 30
+2082 7 29 30 30 29 30 29 30 29 30 29 29 30 30
+2083 0 29 30 30 29 30 29 30 29 30 29 30 29
+2084 0 30 29 30 29 30 30 29 30 29 30 29 30
+2085 5 29 29 30 29 30 30 29 30 29 30 30 29 30
+2086 0 29 29 30 29 30 29 30 30 29 30 30 29
+2087 0 30 29 30 29 29 30 29 30 29 30 30 30
+2088 4 29 30 29 30 29 29 29 30 29 30 30 30 29
+2089 0 30 30 29 30 29 29 29 30 29 30 30 29
+2090 8 30 30 30 29 30 29 29 30 29 29 30 29 30
+2091 0 30 30 29 30 29 30 29 30 29 29 30 29
+2092 0 30 30 29 30 30 29 30 29 30 29 29 30
+2093 6 29 30 29 30 30 29 30 30 29 30 29 30 29
+2094 0 29 30 29 30 29 30 30 29 30 30 29 30
+2095 0 29 29 30 29 30 29 30 29 30 30 30 29
+2096 4 30 29 30 29 29 30 29 29 30 30 30 29 30
+2097 0 30 29 30 29 29 29 30 29 30 30 29 30
+2098 0 30 30 29 30 29 29 29 30 29 30 29 30
+2099 2 30 30 29 30 29 30 29 29 30 29 30 29 30
+2100 0 30 29 30 30 29 30 29 30 29 29 30 29
+2101 7 30 30 29 30 29 30 30 29 30 29 29 30 29
+2102 0 30 29 30 29 30 30 29 30 30 29 30 29
+2103 0 29 30 29 30 29 30 29 30 30 29 30 30
+2104 5 29 29 30 29 30 29 30 29 30 30 29 30 30
+2105 0 29 30 29 29 29 30 29 30 30 29 30 30
+2106 0 30 29 30 29 29 29 30 29 30 29 30 30
+2107 4 30 29 30 30 29 29 29 30 29 30 29 30 30
+2108 0 30 29 30 29 30 29 29 30 29 30 29 30
+2109 9 30 29 30 30 29 30 29 30 29 29 30 29 30
+2110 0 29 30 30 29 30 29 30 30 29 29 30 29
+2111 0 30 29 30 29 30 30 29 30 29 30 29 30
+2112 6 29 30 29 30 29 30 29 30 30 29 30 30 29
+2113 0 29 30 29 30 29 29 30 30 29 30 30 30
+2114 0 29 30 29 29 29 30 29 30 29 30 30 30
+2115 4 29 30 30 29 29 29 30 29 30 29 30 30 30
+2116 0 29 30 29 30 29 29 30 29 30 29 30 30
+2117 0 29 30 30 29 30 29 29 30 29 30 29 30
+2118 3 29 30 30 29 30 30 29 29 30 29 30 29 30
+2119 0 29 30 29 30 30 29 30 29 30 29 30 29
+2120 7 30 29 30 29 30 29 30 30 29 30 29 30 29
+2121 0 30 29 30 29 29 30 30 29 30 30 29 30
+2122 0 29 30 29 29 30 29 30 29 30 30 30 29
+2123 5 30 29 30 29 29 30 29 30 29 30 30 30 29
+2124 0 30 29 30 29 29 30 29 30 29 30 29 30
+2125 0 30 30 29 30 29 29 30 29 30 29 30 29
+2126 4 30 30 30 29 30 29 29 30 29 30 29 30 29
+2127 0 30 30 29 30 29 30 29 30 29 30 29 30
+2128 11 29 30 29 30 30 29 30 29 30 29 30 29 30
+2129 0 29 30 29 30 29 30 30 29 30 29 30 29
+2130 0 30 29 30 29 29 30 30 29 30 30 29 30
+2131 6 29 30 29 29 30 29 30 29 30 30 29 30 30
+2132 0 29 30 29 29 30 29 30 29 30 29 30 30
+2133 0 30 29 30 29 30 29 29 29 30 30 29 30
+2134 5 30 30 29 30 29 29 30 29 29 30 29 30 30
+2135 0 30 29 30 30 29 29 30 29 30 29 29 30
+2136 0 30 29 30 30 29 30 29 30 29 30 29 30
+2137 2 29 30 29 30 29 30 30 29 30 29 30 29 30
+2138 0 29 30 29 29 30 30 29 30 30 29 30 29
+2139 7 30 29 29 30 29 30 29 30 30 29 30 30 29
+2140 0 30 29 29 30 29 30 29 30 29 30 30 30
+2141 0 29 30 29 29 30 29 29 30 30 29 30 30
+2142 5 30 29 30 29 30 29 29 29 30 29 30 30 30
+2143 0 29 30 30 29 29 30 29 29 30 29 30 30
+2144 0 29 30 30 29 30 29 30 29 30 29 29 30
+2145 4 30 29 30 30 29 30 29 30 29 30 29 29 30
+2146 0 29 30 30 29 30 29 30 30 29 30 29 30
+2147 0 29 29 30 29 30 29 30 30 29 30 30 29
+2148 1 30 29 29 30 29 30 29 30 30 29 30 30 29
+2149 0 30 29 29 30 29 30 29 30 29 30 30 30
+2150 6 29 30 29 30 29 29 29 30 29 30 30 30 29
+2151 0 30 30 29 30 29 29 29 30 29 30 30 29
+2152 0 30 30 30 29 29 30 29 29 30 29 30 29
+2153 5 30 30 30 29 30 29 30 29 30 29 29 30 29
+2154 0 30 30 29 30 30 29 30 29 30 29 29 30
+2155 0 29 30 29 30 30 29 30 30 29 30 29 30
+2156 3 29 29 30 29 30 29 30 30 29 30 30 29 30
+2157 0 29 29 30 29 30 29 30 29 30 30 30 29
+2158 7 30 29 29 30 29 29 30 29 30 30 30 29 30
+2159 0 30 29 30 29 29 29 30 29 30 30 29 30
+2160 0 30 30 29 29 30 29 29 30 29 30 29 30
+2161 6 30 30 29 30 29 30 29 29 30 29 30 29 30
+2162 0 30 29 30 30 29 30 29 29 30 29 30 29
+2163 0 30 29 30 30 29 30 30 29 30 29 29 30
+2164 4 29 30 29 30 29 30 30 29 30 30 29 29 30
+2165 0 29 30 29 30 29 30 29 30 30 29 30 30
+2166 10 29 29 30 29 29 30 29 30 30 30 29 30 30
+2167 0 29 29 30 29 29 30 29 30 30 29 30 30
+2168 0 30 29 29 30 29 29 30 29 30 29 30 30
+2169 6 30 29 30 29 30 29 29 30 29 30 29 30 30
+2170 0 29 30 30 29 30 29 29 30 29 30 29 30
+2171 0 29 30 30 30 29 30 29 29 30 29 30 29
+2172 5 30 29 30 30 29 30 29 30 29 30 29 30 29
+2173 0 30 29 30 29 30 30 29 30 29 30 29 30
+2174 0 29 30 29 30 29 30 29 30 30 29 30 30
+2175 3 29 29 30 29 29 30 29 30 30 29 30 30 30
+2176 0 29 29 30 29 29 30 29 30 29 30 30 30
+2177 7 29 30 29 30 29 29 30 29 30 29 30 30 30
+2178 0 29 30 29 30 29 29 30 29 30 29 30 30
+2179 0 29 30 30 29 30 29 29 30 29 30 29 30
+2180 6 29 30 30 29 30 30 29 29 30 29 30 29 30
+2181 0 29 30 29 30 30 29 30 29 30 29 30 29
+2182 0 30 29 30 29 30 29 30 30 29 30 29 30
+2183 4 29 30 29 30 29 29 30 30 29 30 30 29 30
+2184 0 29 30 29 29 30 29 30 29 30 30 30 29
+2185 0 30 29 30 29 29 30 29 30 29 30 30 30
+2186 2 29 30 29 30 29 29 30 29 29 30 30 29 30
+2187 0 30 30 29 30 29 29 30 29 29 30 30 29
+2188 6 30 30 30 29 30 29 29 30 29 30 29 30 29
+2189 0 30 30 29 30 29 30 29 30 29 30 29 30
+2190 0 29 30 29 30 30 29 30 29 30 29 30 29
+2191 5 30 29 30 29 30 29 30 29 30 30 29 30 29
+2192 0 30 29 29 30 29 30 29 30 30 30 29 30
+2193 0 29 30 29 29 30 29 30 29 30 30 29 30
+2194 3 30 29 30 29 29 30 29 29 30 30 29 30 30
+2195 0 30 29 30 29 29 30 29 29 30 30 29 30
+2196 7 30 30 29 30 29 29 30 29 29 30 29 30 30
+2197 0 30 29 30 30 29 29 30 29 29 30 29 30
+2198 0 30 29 30 30 29 30 29 30 29 30 29 30
+2199 6 29 30 29 30 29 30 30 29 30 29 30 29 30