diff --git a/cryptor/rsa_private_example.pem b/cryptor/rsa_private_example.pem index c31daff..b635332 100644 --- a/cryptor/rsa_private_example.pem +++ b/cryptor/rsa_private_example.pem @@ -1,51 +1,51 @@ -----BEGIN rsa private key----- -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 -----END rsa private key----- diff --git a/cryptor/rsa_public_example.pem b/cryptor/rsa_public_example.pem index ec76f1d..2180493 100644 --- a/cryptor/rsa_public_example.pem +++ b/cryptor/rsa_public_example.pem @@ -1,14 +1,14 @@ -----BEGIN rsa public key----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5bsUtFZmae/9D6ncFO62 -EaaGxoc0+/ZXqi2OtHZYpaDV0/bLV0gegTIQ5+kUJr0VQ/9GMEnALdGNBT9bJ0Mv -7VWZijYoyUOQ4fB4k8vaytFcug/kMYkvE+pvXg6JMbp9rZXXiz5djabAKv9ZrClU -ugnK8KPtn+6SnCC4oKVhMRj2G+5LAxYhiidSrnK8xkgts4OwLTzqWw9N2QMvdkXj -wwaIG5UeDqeal0/hna402j6/QbANq/brR2eXHfxVrX9eMKBdA6OmVPeFUVb+JR/p -2TvNEEXRM/DmPP70HXg08L1XIjvaZm7MNbcqS+6AUciwWNshm8o4vFkyjN1E3U5s -yYptCEtDYA/+GPFzbzCzSyaTxBpdxSI6wB9Vt+Hk/DzS3RBoxj4xYk2LmPDX6i86 -skBTQ9X3sUdWUoelyAko8JW+sXlXMQP05yNyBY+fayzhHfYYsi+4jpV4uLMY7bXe -ySffROdiEQ9xXCf3trq2isKReHhYhmu7mnjIn+CcJZ909XzMJmN6zEeSTeXZMDqP -K1dv3Yom6n/oRpR+rfx8JYCKTFR/+QYPGR1kc+LUyqgFcWwTexf3CfL3ck0H0iG7 -ivGV6wm9xvpOxbQb+4/79jQLtoBC8goU9KXuxYEs2aE4/LM8zY4QpHDIZYLoHnz2 -xSgm+rlh6E4p0t5mik3wOAUCAwEAAQ== +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAydCMlSbzx3yb6o3T/CUp +fxRHSl45LjyqhEQiMHkGPcL9d81NlxVHvvy/V/mY5wtarYRlifsk2OOVJ2LyfagX +KM5k16LEEq2PsPcKVM4Vp6gLCIB2aX88hfJkYfTRGW2RX9aVCJaK1Ak2nXInuNEG +9T/ykelH57gHjH2k9NuDBifcIm3fIKNKYqK4WvDQMob8KyKJdiKPsKll4Z5ZKGt/ +gqpMt0gWx1fZ/njUnGATUoXMH7/JXdPQH+VVYt5HFq8JqffkiiIW0zzdNmA0dSGx +c96i8dCNdIB79iQAJ/X0c5fk5/8b/fkLUHY/BvBMhFtLp7/TCnbzn5Rmp8IJtWKe +xaQ00AqzRNGnd4LoJAiPWAipfsUeYXW517lBllKz9wiKo7AmkB0h8fig5ZEK5btS +BqNRgdnMjBxrm/QFgWdqsxmzcjrXD3iV+ZNOE8s47jkdhjoPRpqqyMSUawmJs1uy +SrVpgaldIJrBzNOATReHFQ+mSlHi7D1BcrOegCHasIqaQKv+w9ttR1P6bFcVezO/ +OQaTbdQviB+pHqYv7Ww9kVPko1c+X5SMUUXy0MwwjLRl2TcEA0BzUf3Qdq5TA7x2 +Q4Eak2oBsuuEZrCPSTDRXjwUxd+Cz82SZMKg6fDe409dPR0S8w1xp4gdknvoQIyN +pNrY6I6/UyDieOAImOLSrMECAwEAAQ== -----END rsa public key----- diff --git a/datetime/datetime.go b/datetime/datetime.go index d61fa67..090caf1 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -100,6 +100,61 @@ func AddYear(t time.Time, year int64) time.Time { return t.AddDate(int(year), 0, 0) } +// AddDaySafe add or sub days to the time and ensure that the returned date does not exceed the valid date of the target year and month. +// Play: todo +func AddDaySafe(t time.Time, days int) time.Time { + t = t.AddDate(0, 0, days) + year, month, day := t.Date() + + lastDayOfMonth := time.Date(year, month+1, 0, 0, 0, 0, 0, t.Location()).Day() + + if day > lastDayOfMonth { + t = time.Date(year, month, lastDayOfMonth, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()) + } + + return t +} + +// AddMonthSafe add or sub months to the time and ensure that the returned date does not exceed the valid date of the target year and month. +// Play: todo +func AddMonthSafe(t time.Time, months int) time.Time { + year := t.Year() + month := int(t.Month()) + months + + for month > 12 { + month -= 12 + year++ + } + for month < 1 { + month += 12 + year-- + } + + daysInMonth := time.Date(year, time.Month(month+1), 0, 0, 0, 0, 0, time.UTC).Day() + + day := t.Day() + if day > daysInMonth { + day = daysInMonth + } + + return time.Date(year, time.Month(month), day, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()) +} + +// AddYearSafe add or sub years to the time and ensure that the returned date does not exceed the valid date of the target year and month. +// Play: todo +func AddYearSafe(t time.Time, years int) time.Time { + year, month, day := t.Date() + year += years + + if month == time.February && day == 29 { + if !IsLeapYear(year) { + day = 28 + } + } + + return time.Date(year, month, day, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()) +} + // GetNowDate return format yyyy-mm-dd of current date. // Play: https://go.dev/play/p/PvfkPpcpBBf func GetNowDate() string { diff --git a/datetime/datetime_example_test.go b/datetime/datetime_example_test.go index 7b3fec7..d3deee1 100644 --- a/datetime/datetime_example_test.go +++ b/datetime/datetime_example_test.go @@ -90,6 +90,60 @@ func ExampleAddYear() { // 2019-01-01 } +func ExampleAddDaySafe() { + leapYearDate1, _ := time.Parse("2006-01-02", "2024-02-29") + result1 := AddDaySafe(leapYearDate1, 1) + + leapYearDate2, _ := time.Parse("2006-01-02", "2024-03-01") + result2 := AddDaySafe(leapYearDate2, -1) + + nonLeapYearDate1, _ := time.Parse("2006-01-02", "2025-02-28") + result3 := AddDaySafe(nonLeapYearDate1, 1) + + nonLeaYearDate2, _ := time.Parse("2006-01-02", "2025-03-01") + result4 := AddDaySafe(nonLeaYearDate2, -1) + + fmt.Println(result1.Format("2006-01-02")) + fmt.Println(result2.Format("2006-01-02")) + fmt.Println(result3.Format("2006-01-02")) + fmt.Println(result4.Format("2006-01-02")) + + // Output: + // 2024-03-01 + // 2024-02-29 + // 2025-03-01 + // 2025-02-28 +} + +func ExampleAddMonthSafe() { + date1, _ := time.Parse("2006-01-02", "2025-01-31") + result1 := AddMonthSafe(date1, 1) + + date2, _ := time.Parse("2006-01-02", "2024-02-29") + result2 := AddMonthSafe(date2, -1) + + fmt.Println(result1.Format("2006-01-02")) + fmt.Println(result2.Format("2006-01-02")) + + // Output: + // 2025-02-28 + // 2024-01-29 +} + +func ExampleAddYearSafe() { + date, _ := time.Parse("2006-01-02", "2020-02-29") + + result1 := AddYearSafe(date, 1) + result2 := AddYearSafe(date, -1) + + fmt.Println(result1.Format("2006-01-02")) + fmt.Println(result2.Format("2006-01-02")) + + // Output: + // 2021-02-28 + // 2019-02-28 +} + func ExampleGetNowDate() { result := GetNowDate() diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index a9e19b1..54e933b 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -309,6 +309,129 @@ func TestAddMonth(t *testing.T) { } +func TestAddDaySafe(t *testing.T) { + t.Parallel() + + assert := internal.NewAssert(t, "TestAddDaySafe") + + tests := []struct { + inputDate string + days int + expected string + }{ + {"2025-01-31", 10, "2025-02-10"}, + {"2025-01-01", 30, "2025-01-31"}, + {"2025-01-31", 1, "2025-02-01"}, + {"2025-02-28", 1, "2025-03-01"}, + {"2024-02-29", 1, "2024-03-01"}, + {"2024-02-29", 365, "2025-02-28"}, + + {"2025-01-31", -10, "2025-01-21"}, + {"2025-01-01", -30, "2024-12-02"}, + {"2025-02-01", -1, "2025-01-31"}, + {"2025-03-01", -1, "2025-02-28"}, + {"2024-03-01", -1, "2024-02-29"}, + + {"2025-01-31", -31, "2024-12-31"}, + {"2025-12-31", 1, "2026-01-01"}, + } + + for _, tt := range tests { + date, _ := time.Parse("2006-01-02", tt.inputDate) + result := AddDaySafe(date, tt.days) + assert.Equal(tt.expected, result.Format("2006-01-02")) + } +} + +func TestAddMonthSafe(t *testing.T) { + t.Parallel() + + assert := internal.NewAssert(t, "TestAddMonthSafe") + + tests := []struct { + inputDate string + months int + expected string + }{ + { + inputDate: "2025-01-31", + months: 1, + expected: "2025-02-28", + }, + { + inputDate: "2025-01-31", + months: -1, + expected: "2024-12-31", + }, + { + inputDate: "2025-12-31", + months: 1, + expected: "2026-01-31", + }, + { + inputDate: "2025-01-31", + months: -1, + expected: "2024-12-31", + }, + { + inputDate: "2024-02-29", + months: 1, + expected: "2024-03-29", + }, + { + inputDate: "2024-02-29", + months: -1, + expected: "2024-01-29", + }, + } + + for _, tt := range tests { + date, _ := time.Parse("2006-01-02", tt.inputDate) + result := AddMonthSafe(date, tt.months) + assert.Equal(tt.expected, result.Format("2006-01-02")) + } + +} + +func TestAddYearSafe(t *testing.T) { + t.Parallel() + + assert := internal.NewAssert(t, "TestAddYearSafe") + + tests := []struct { + inputDate string + years int + expected string + }{ + { + inputDate: "2020-02-29", + years: 1, + expected: "2021-02-28", + }, + { + inputDate: "2020-02-29", + years: 2, + expected: "2022-02-28", + }, + { + inputDate: "2020-02-29", + years: -1, + expected: "2019-02-28", + }, + { + inputDate: "2020-02-29", + years: -2, + expected: "2018-02-28", + }, + } + + for _, tt := range tests { + date, _ := time.Parse("2006-01-02", tt.inputDate) + result := AddYearSafe(date, tt.years) + assert.Equal(tt.expected, result.Format("2006-01-02")) + } +} + func TestGetNowDate(t *testing.T) { t.Parallel()