mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 03:13:46 +00:00
/amlich converts duong lich to am lich (defaults to today, Asia/Saigon); /duonglich converts am lich to duong lich with a nhuan flag for leap months. Dates accept d, d/m, or d/m/yyyy - missing parts fill from today in the input's calendar. Conversion is a dependency-free port of Ho Ngoc Duc's algorithm at UTC+7, with rejection of impossible lunar inputs, anchored by known Tet/leap-month dates and a 1950-2050 round-trip test.
155 lines
5.8 KiB
Go
155 lines
5.8 KiB
Go
package amlich
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-telegram/bot"
|
|
"github.com/go-telegram/bot/models"
|
|
|
|
"github.com/tiennm99/miti99bot/internal/modules"
|
|
"github.com/tiennm99/miti99bot/internal/modules/util/chathelper"
|
|
)
|
|
|
|
// saigonLocation pins "today" for /amlich without an argument. FixedZone (not
|
|
// LoadLocation) matches the rest of the repo and needs no tzdata in the
|
|
// runtime image; Vietnam has no DST so the fixed offset is always correct.
|
|
var saigonLocation = time.FixedZone("Asia/Saigon", 7*60*60)
|
|
|
|
const amlichUsage = `Cách dùng: /amlich [dd[/mm[/yyyy]]] — đổi dương lịch sang âm lịch, bỏ trống để xem hôm nay; thiếu tháng/năm thì dùng tháng/năm hiện tại. Ví dụ: /amlich 29/01/2025`
|
|
|
|
const duonglichUsage = `Cách dùng: /duonglich <dd[/mm[/yyyy]]> [nhuan] — đổi âm lịch sang dương lịch, thiếu tháng/năm thì dùng tháng/năm âm lịch hiện tại; thêm "nhuan" nếu là tháng nhuận. Ví dụ: /duonglich 01/01/2025`
|
|
|
|
// yearRangeMessage is shared by both commands; the bound lives in lunar.go
|
|
// next to the algorithm it protects.
|
|
var yearRangeMessage = fmt.Sprintf("Chỉ hỗ trợ các năm từ %d đến %d.", minLunarYear, maxLunarYear)
|
|
|
|
func amlichCommand() modules.Command {
|
|
return modules.Command{
|
|
Name: "amlich",
|
|
Visibility: modules.VisibilityPublic,
|
|
Description: "Đổi ngày dương lịch sang âm lịch (bỏ trống: hôm nay)",
|
|
Parameters: "[date]",
|
|
Handler: func(ctx context.Context, b *bot.Bot, update *models.Update) error {
|
|
if update.Message == nil {
|
|
return nil
|
|
}
|
|
now := time.Now().In(saigonLocation)
|
|
var day, month, year int
|
|
arg := strings.TrimSpace(chathelper.ArgAfterCommand(update.Message.Text))
|
|
if arg == "" {
|
|
day, month, year = now.Day(), int(now.Month()), now.Year()
|
|
} else {
|
|
var ok bool
|
|
day, month, year, ok = parseDate(arg, int(now.Month()), now.Year())
|
|
if !ok || !isValidSolarDate(day, month, year) {
|
|
return chathelper.Reply(ctx, b, update.Message, amlichUsage)
|
|
}
|
|
}
|
|
if year < minLunarYear || year > maxLunarYear {
|
|
return chathelper.Reply(ctx, b, update.Message, yearRangeMessage)
|
|
}
|
|
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)
|
|
return chathelper.Reply(ctx, b, update.Message, text)
|
|
},
|
|
}
|
|
}
|
|
|
|
func duonglichCommand() modules.Command {
|
|
return modules.Command{
|
|
Name: "duonglich",
|
|
Visibility: modules.VisibilityPublic,
|
|
Description: "Đổi ngày âm lịch sang dương lịch",
|
|
Parameters: "<date> [nhuan]",
|
|
Handler: func(ctx context.Context, b *bot.Bot, update *models.Update) error {
|
|
if update.Message == nil {
|
|
return nil
|
|
}
|
|
fields := strings.Fields(chathelper.ArgAfterCommand(update.Message.Text))
|
|
if len(fields) == 0 || len(fields) > 2 {
|
|
return chathelper.Reply(ctx, b, update.Message, duonglichUsage)
|
|
}
|
|
// Missing month/year fall back to today's date expressed in the
|
|
// same calendar as the input — the current lunar month and year.
|
|
now := time.Now().In(saigonLocation)
|
|
_, nowLunarMonth, nowLunarYear, _ := solarToLunar(now.Day(), int(now.Month()), now.Year())
|
|
day, month, year, ok := parseDate(fields[0], nowLunarMonth, nowLunarYear)
|
|
// Lunar months never exceed 30 days; deeper validity (day 30 of a
|
|
// 29-day month, leap-month existence) is checked by lunarToSolar.
|
|
if !ok || month < 1 || month > 12 || day < 1 || day > 30 {
|
|
return chathelper.Reply(ctx, b, update.Message, duonglichUsage)
|
|
}
|
|
leap := false
|
|
if len(fields) == 2 {
|
|
switch strings.ToLower(fields[1]) {
|
|
case "nhuan", "nhuận":
|
|
leap = true
|
|
default:
|
|
return chathelper.Reply(ctx, b, update.Message, duonglichUsage)
|
|
}
|
|
}
|
|
if year < minLunarYear || year > maxLunarYear {
|
|
return chathelper.Reply(ctx, b, update.Message, yearRangeMessage)
|
|
}
|
|
solarDay, solarMonth, solarYear, err := lunarToSolar(day, month, year, leap)
|
|
if err != nil {
|
|
return chathelper.Reply(ctx, b, update.Message, "Không đổi được: "+err.Error()+".")
|
|
}
|
|
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)
|
|
return chathelper.Reply(ctx, b, update.Message, text)
|
|
},
|
|
}
|
|
}
|
|
|
|
// parseDate parses "d", "d/m", or "d/m/yyyy" (leading zeros optional, digits
|
|
// only); a missing month or year is filled from defMonth/defYear — today in
|
|
// the calendar the caller works in. It checks numeric shape only; calendar
|
|
// validity is the caller's job because solar and lunar dates have different
|
|
// rules.
|
|
func parseDate(s string, defMonth, defYear int) (day, month, year int, ok bool) {
|
|
parts := strings.Split(s, "/")
|
|
if len(parts) < 1 || len(parts) > 3 {
|
|
return 0, 0, 0, false
|
|
}
|
|
nums := make([]int, 0, 3)
|
|
for _, part := range parts {
|
|
if part == "" || len(part) > 4 {
|
|
return 0, 0, 0, false
|
|
}
|
|
for _, r := range part {
|
|
if r < '0' || r > '9' {
|
|
return 0, 0, 0, false
|
|
}
|
|
}
|
|
n, err := strconv.Atoi(part)
|
|
if err != nil {
|
|
return 0, 0, 0, false
|
|
}
|
|
nums = append(nums, n)
|
|
}
|
|
day, month, year = nums[0], defMonth, defYear
|
|
if len(nums) >= 2 {
|
|
month = nums[1]
|
|
}
|
|
if len(nums) == 3 {
|
|
year = nums[2]
|
|
}
|
|
return day, month, year, true
|
|
}
|
|
|
|
// isValidSolarDate reports whether the Gregorian date exists, using
|
|
// time.Date's normalization (31/02 rolls into March → mismatch → invalid).
|
|
func isValidSolarDate(day, month, year int) bool {
|
|
if month < 1 || month > 12 || day < 1 {
|
|
return false
|
|
}
|
|
t := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
|
|
return t.Day() == day && int(t.Month()) == month && t.Year() == year
|
|
}
|