427 lines
10 KiB
Go
427 lines
10 KiB
Go
|
|
package quando
|
||
|
|
|
||
|
|
import (
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestAddSeconds(t *testing.T) {
|
||
|
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||
|
|
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
value int
|
||
|
|
expected string
|
||
|
|
}{
|
||
|
|
{"add 1 second", 1, "2026-01-01 12:00:01"},
|
||
|
|
{"add 60 seconds", 60, "2026-01-01 12:01:00"},
|
||
|
|
{"add 3600 seconds", 3600, "2026-01-01 13:00:00"},
|
||
|
|
{"subtract 1 second", -1, "2026-01-01 11:59:59"},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
result := date.Add(tt.value, Seconds)
|
||
|
|
if result.String() != tt.expected {
|
||
|
|
t.Errorf("Add(%d, Seconds) = %v, want %v", tt.value, result, tt.expected)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAddMinutes(t *testing.T) {
|
||
|
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||
|
|
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
value int
|
||
|
|
expected string
|
||
|
|
}{
|
||
|
|
{"add 1 minute", 1, "2026-01-01 12:01:00"},
|
||
|
|
{"add 60 minutes", 60, "2026-01-01 13:00:00"},
|
||
|
|
{"subtract 1 minute", -1, "2026-01-01 11:59:00"},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
result := date.Add(tt.value, Minutes)
|
||
|
|
if result.String() != tt.expected {
|
||
|
|
t.Errorf("Add(%d, Minutes) = %v, want %v", tt.value, result, tt.expected)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAddHours(t *testing.T) {
|
||
|
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||
|
|
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
value int
|
||
|
|
expected string
|
||
|
|
}{
|
||
|
|
{"add 1 hour", 1, "2026-01-01 13:00:00"},
|
||
|
|
{"add 24 hours", 24, "2026-01-02 12:00:00"},
|
||
|
|
{"subtract 1 hour", -1, "2026-01-01 11:00:00"},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
result := date.Add(tt.value, Hours)
|
||
|
|
if result.String() != tt.expected {
|
||
|
|
t.Errorf("Add(%d, Hours) = %v, want %v", tt.value, result, tt.expected)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAddDays(t *testing.T) {
|
||
|
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||
|
|
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
value int
|
||
|
|
expected string
|
||
|
|
}{
|
||
|
|
{"add 1 day", 1, "2026-01-02 12:00:00"},
|
||
|
|
{"add 7 days", 7, "2026-01-08 12:00:00"},
|
||
|
|
{"add 365 days", 365, "2027-01-01 12:00:00"},
|
||
|
|
{"subtract 1 day", -1, "2025-12-31 12:00:00"},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
result := date.Add(tt.value, Days)
|
||
|
|
if result.String() != tt.expected {
|
||
|
|
t.Errorf("Add(%d, Days) = %v, want %v", tt.value, result, tt.expected)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAddWeeks(t *testing.T) {
|
||
|
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||
|
|
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
value int
|
||
|
|
expected string
|
||
|
|
}{
|
||
|
|
{"add 1 week", 1, "2026-01-08 12:00:00"},
|
||
|
|
{"add 4 weeks", 4, "2026-01-29 12:00:00"},
|
||
|
|
{"subtract 1 week", -1, "2025-12-25 12:00:00"},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
result := date.Add(tt.value, Weeks)
|
||
|
|
if result.String() != tt.expected {
|
||
|
|
t.Errorf("Add(%d, Weeks) = %v, want %v", tt.value, result, tt.expected)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAddMonths(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
date Date
|
||
|
|
months int
|
||
|
|
expected string
|
||
|
|
}{
|
||
|
|
// Regular month addition (no overflow)
|
||
|
|
{
|
||
|
|
name: "regular addition",
|
||
|
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: 1,
|
||
|
|
expected: "2026-02-15 12:00:00",
|
||
|
|
},
|
||
|
|
// Month-end overflow cases
|
||
|
|
{
|
||
|
|
name: "Jan 31 + 1 month = Feb 28 (non-leap year)",
|
||
|
|
date: From(time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: 1,
|
||
|
|
expected: "2026-02-28 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Jan 31 + 1 month = Feb 29 (leap year)",
|
||
|
|
date: From(time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: 1,
|
||
|
|
expected: "2024-02-29 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "May 31 + 1 month = Jun 30",
|
||
|
|
date: From(time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: 1,
|
||
|
|
expected: "2026-06-30 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Jul 31 + 1 month = Aug 31",
|
||
|
|
date: From(time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: 1,
|
||
|
|
expected: "2026-08-31 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Aug 31 + 1 month = Sep 30",
|
||
|
|
date: From(time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: 1,
|
||
|
|
expected: "2026-09-30 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Oct 31 + 1 month = Nov 30",
|
||
|
|
date: From(time.Date(2026, 10, 31, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: 1,
|
||
|
|
expected: "2026-11-30 12:00:00"},
|
||
|
|
// Cross year boundary
|
||
|
|
{
|
||
|
|
name: "Dec 15 + 1 month = Jan 15 next year",
|
||
|
|
date: From(time.Date(2026, 12, 15, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: 1,
|
||
|
|
expected: "2027-01-15 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Dec 31 + 1 month = Jan 31 next year",
|
||
|
|
date: From(time.Date(2026, 12, 31, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: 1,
|
||
|
|
expected: "2027-01-31 12:00:00",
|
||
|
|
},
|
||
|
|
// Multiple months
|
||
|
|
{
|
||
|
|
name: "Jan 31 + 2 months = Mar 31",
|
||
|
|
date: From(time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: 2,
|
||
|
|
expected: "2026-03-31 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Jan 31 + 13 months = Feb 28 next year",
|
||
|
|
date: From(time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: 13,
|
||
|
|
expected: "2027-02-28 12:00:00",
|
||
|
|
},
|
||
|
|
// Negative values (subtraction)
|
||
|
|
{
|
||
|
|
name: "Mar 31 - 1 month = Feb 28",
|
||
|
|
date: From(time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: -1,
|
||
|
|
expected: "2026-02-28 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Jan 15 - 1 month = Dec 15 prev year",
|
||
|
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||
|
|
months: -1,
|
||
|
|
expected: "2025-12-15 12:00:00",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
result := tt.date.Add(tt.months, Months)
|
||
|
|
if result.String() != tt.expected {
|
||
|
|
t.Errorf("Add(%d, Months) = %v, want %v", tt.months, result, tt.expected)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAddQuarters(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
date Date
|
||
|
|
quarters int
|
||
|
|
expected string
|
||
|
|
}{
|
||
|
|
{
|
||
|
|
name: "add 1 quarter",
|
||
|
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||
|
|
quarters: 1,
|
||
|
|
expected: "2026-04-15 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "add 4 quarters (1 year)",
|
||
|
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||
|
|
quarters: 4,
|
||
|
|
expected: "2027-01-15 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "quarter with month-end overflow",
|
||
|
|
date: From(time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC)),
|
||
|
|
quarters: 1,
|
||
|
|
expected: "2026-08-31 12:00:00",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
result := tt.date.Add(tt.quarters, Quarters)
|
||
|
|
if result.String() != tt.expected {
|
||
|
|
t.Errorf("Add(%d, Quarters) = %v, want %v", tt.quarters, result, tt.expected)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAddYears(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
date Date
|
||
|
|
years int
|
||
|
|
expected string
|
||
|
|
}{
|
||
|
|
{
|
||
|
|
name: "add 1 year",
|
||
|
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||
|
|
years: 1,
|
||
|
|
expected: "2027-01-15 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "add 10 years",
|
||
|
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||
|
|
years: 10,
|
||
|
|
expected: "2036-01-15 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "leap year to non-leap year (Feb 29 -> Feb 28)",
|
||
|
|
date: From(time.Date(2024, 2, 29, 12, 0, 0, 0, time.UTC)),
|
||
|
|
years: 1,
|
||
|
|
expected: "2025-02-28 12:00:00",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "subtract 1 year",
|
||
|
|
date: From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)),
|
||
|
|
years: -1,
|
||
|
|
expected: "2025-01-15 12:00:00",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
result := tt.date.Add(tt.years, Years)
|
||
|
|
if result.String() != tt.expected {
|
||
|
|
t.Errorf("Add(%d, Years) = %v, want %v", tt.years, result, tt.expected)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSub(t *testing.T) {
|
||
|
|
date := From(time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC))
|
||
|
|
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
value int
|
||
|
|
unit Unit
|
||
|
|
expected string
|
||
|
|
}{
|
||
|
|
{"subtract 1 day", 1, Days, "2026-03-30 12:00:00"},
|
||
|
|
{"subtract 1 month", 1, Months, "2026-02-28 12:00:00"},
|
||
|
|
{"subtract 1 year", 1, Years, "2025-03-31 12:00:00"},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
result := date.Sub(tt.value, tt.unit)
|
||
|
|
if result.String() != tt.expected {
|
||
|
|
t.Errorf("Sub(%d, %v) = %v, want %v", tt.value, tt.unit, result, tt.expected)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAddSubEquivalence(t *testing.T) {
|
||
|
|
date := From(time.Date(2026, 3, 15, 12, 0, 0, 0, time.UTC))
|
||
|
|
|
||
|
|
// Sub(n) should equal Add(-n)
|
||
|
|
units := []Unit{Seconds, Minutes, Hours, Days, Weeks, Months, Quarters, Years}
|
||
|
|
|
||
|
|
for _, unit := range units {
|
||
|
|
result1 := date.Sub(5, unit)
|
||
|
|
result2 := date.Add(-5, unit)
|
||
|
|
|
||
|
|
if !result1.Time().Equal(result2.Time()) {
|
||
|
|
t.Errorf("Sub(5, %v) != Add(-5, %v): %v != %v", unit, unit, result1, result2)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMethodChaining(t *testing.T) {
|
||
|
|
date := From(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
|
||
|
|
|
||
|
|
// Chain multiple operations
|
||
|
|
result := date.
|
||
|
|
Add(1, Months).
|
||
|
|
Add(15, Days).
|
||
|
|
Sub(2, Hours)
|
||
|
|
|
||
|
|
expected := "2026-02-16 10:00:00"
|
||
|
|
if result.String() != expected {
|
||
|
|
t.Errorf("Chained operations = %v, want %v", result, expected)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestImmutability(t *testing.T) {
|
||
|
|
original := From(time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC))
|
||
|
|
originalTime := original.Time()
|
||
|
|
|
||
|
|
// Perform various operations
|
||
|
|
_ = original.Add(1, Days)
|
||
|
|
_ = original.Add(1, Months)
|
||
|
|
_ = original.Sub(1, Years)
|
||
|
|
|
||
|
|
// Verify original is unchanged
|
||
|
|
if !original.Time().Equal(originalTime) {
|
||
|
|
t.Error("Add/Sub operations modified the original date")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestTimezonePreservation verifies that arithmetic preserves timezone
|
||
|
|
func TestTimezonePreservation(t *testing.T) {
|
||
|
|
loc, err := time.LoadLocation("Europe/Berlin")
|
||
|
|
if err != nil {
|
||
|
|
t.Skipf("Skipping timezone test: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
berlinTime := time.Date(2026, 1, 15, 12, 0, 0, 0, loc)
|
||
|
|
date := From(berlinTime)
|
||
|
|
|
||
|
|
result := date.Add(1, Months)
|
||
|
|
|
||
|
|
if result.Time().Location() != loc {
|
||
|
|
t.Errorf("Add() location = %v, want %v", result.Time().Location(), loc)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// BenchmarkAddDays benchmarks Add with Days
|
||
|
|
func BenchmarkAddDays(b *testing.B) {
|
||
|
|
date := Now()
|
||
|
|
b.ResetTimer()
|
||
|
|
for i := 0; i < b.N; i++ {
|
||
|
|
_ = date.Add(1, Days)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// BenchmarkAddMonths benchmarks Add with Months
|
||
|
|
func BenchmarkAddMonths(b *testing.B) {
|
||
|
|
date := Now()
|
||
|
|
b.ResetTimer()
|
||
|
|
for i := 0; i < b.N; i++ {
|
||
|
|
_ = date.Add(1, Months)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// BenchmarkAddYears benchmarks Add with Years
|
||
|
|
func BenchmarkAddYears(b *testing.B) {
|
||
|
|
date := Now()
|
||
|
|
b.ResetTimer()
|
||
|
|
for i := 0; i < b.N; i++ {
|
||
|
|
_ = date.Add(1, Years)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// BenchmarkMethodChaining benchmarks chained operations
|
||
|
|
func BenchmarkMethodChaining(b *testing.B) {
|
||
|
|
date := Now()
|
||
|
|
b.ResetTimer()
|
||
|
|
for i := 0; i < b.N; i++ {
|
||
|
|
_ = date.Add(1, Months).Add(15, Days).Sub(2, Hours)
|
||
|
|
}
|
||
|
|
}
|