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:
tiennm99 committed 2026-08-18 23:00:31 +07:00
1 parent f475a5cb46
commit 86c52170de
7 files changed
+669 -12

No files matched your search

+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 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))
}
+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