feat(amlich): hint ambiguous leap-month input and flag disputed month boundaries

- /duonglich appends a nhuan hint when the queried month is also that
  lunar year's leap month and the exact leap date exists
- both commands append a caveat when the result falls in a lunar month
  starting or ending on one of the seven razor-edge boundaries from
  2072 on (new moon within ~2 minutes of UTC+7 midnight)
- freeze the verified 1800-2199 month structure as golden testdata so
  a self-consistent engine change cannot silently shift boundaries
- close known-issues open questions 1 and 2
This commit is contained in:
2026-08-18 23:00:31 +07:00
parent 43589d3fe4
commit 027a87f3c0
7 changed files with 669 additions and 12 deletions
+23 -12
View File
@@ -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 194567, but the South used UTC+7 until 1959 and
UTC+8 only from 196067, 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.
+105
View File
@@ -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 18002199 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))
}
+23
View File
@@ -27,6 +27,15 @@ const duonglichUsage = `Cách dùng: /duonglich <dd[/mm[/yyyy]]> [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)
},
}
+50
View File
@@ -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",
+30
View File
@@ -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ý"}
+38
View File
@@ -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",
@@ -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