package quando import ( "math" "testing" "time" ) func TestDiff(t *testing.T) { start := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) end := time.Date(2026, 12, 31, 18, 0, 0, 0, time.UTC) dur := Diff(start, end) if dur.start != start { t.Errorf("Diff() start = %v, want %v", dur.start, start) } if dur.end != end { t.Errorf("Diff() end = %v, want %v", dur.end, end) } } func TestDurationSeconds(t *testing.T) { tests := []struct { name string start time.Time end time.Time expected int64 }{ { name: "1 minute", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 1, 0, 1, 0, 0, time.UTC), expected: 60, }, { name: "1 hour", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 1, 1, 0, 0, 0, time.UTC), expected: 3600, }, { name: "negative duration", start: time.Date(2026, 1, 1, 1, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), expected: -3600, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dur := Diff(tt.start, tt.end) result := dur.Seconds() if result != tt.expected { t.Errorf("Seconds() = %d, want %d", result, tt.expected) } }) } } func TestDurationMinutes(t *testing.T) { tests := []struct { name string start time.Time end time.Time expected int64 }{ { name: "1 hour", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 1, 1, 0, 0, 0, time.UTC), expected: 60, }, { name: "1 day", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC), expected: 1440, }, { name: "negative duration", start: time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), expected: -1440, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dur := Diff(tt.start, tt.end) result := dur.Minutes() if result != tt.expected { t.Errorf("Minutes() = %d, want %d", result, tt.expected) } }) } } func TestDurationHours(t *testing.T) { tests := []struct { name string start time.Time end time.Time expected int64 }{ { name: "1 day", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC), expected: 24, }, { name: "1 week", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 8, 0, 0, 0, 0, time.UTC), expected: 168, }, { name: "negative duration", start: time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), expected: -24, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dur := Diff(tt.start, tt.end) result := dur.Hours() if result != tt.expected { t.Errorf("Hours() = %d, want %d", result, tt.expected) } }) } } func TestDurationDays(t *testing.T) { tests := []struct { name string start time.Time end time.Time expected int }{ { name: "1 day", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC), expected: 1, }, { name: "7 days", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 8, 0, 0, 0, 0, time.UTC), expected: 7, }, { name: "365 days", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), expected: 365, }, { name: "leap year (366 days)", start: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), expected: 366, }, { name: "negative duration", start: time.Date(2026, 1, 8, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), expected: -7, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dur := Diff(tt.start, tt.end) result := dur.Days() if result != tt.expected { t.Errorf("Days() = %d, want %d", result, tt.expected) } }) } } func TestDurationWeeks(t *testing.T) { tests := []struct { name string start time.Time end time.Time expected int }{ { name: "1 week", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 8, 0, 0, 0, 0, time.UTC), expected: 1, }, { name: "4 weeks", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 29, 0, 0, 0, 0, time.UTC), expected: 4, }, { name: "52 weeks", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), expected: 52, }, { name: "negative duration", start: time.Date(2026, 1, 29, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), expected: -4, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dur := Diff(tt.start, tt.end) result := dur.Weeks() if result != tt.expected { t.Errorf("Weeks() = %d, want %d", result, tt.expected) } }) } } func TestDurationMonths(t *testing.T) { tests := []struct { name string start time.Time end time.Time expected int }{ { name: "1 month", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), expected: 1, }, { name: "11 months", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC), expected: 11, }, { name: "12 months", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), expected: 12, }, { name: "13 months", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2027, 2, 1, 0, 0, 0, 0, time.UTC), expected: 13, }, { name: "month-end to month-end", start: time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 2, 28, 0, 0, 0, 0, time.UTC), expected: 0, // Less than a full month }, { name: "month-end across full month", start: time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 3, 31, 0, 0, 0, 0, time.UTC), expected: 2, }, { name: "leap year February", start: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC), expected: 1, }, { name: "negative duration", start: time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), expected: -11, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dur := Diff(tt.start, tt.end) result := dur.Months() if result != tt.expected { t.Errorf("Months() = %d, want %d", result, tt.expected) } }) } } func TestDurationYears(t *testing.T) { tests := []struct { name string start time.Time end time.Time expected int }{ { name: "1 year", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), expected: 1, }, { name: "2 years", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2028, 1, 1, 0, 0, 0, 0, time.UTC), expected: 2, }, { name: "11 months (less than 1 year)", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC), expected: 0, }, { name: "13 months (1 year)", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2027, 2, 1, 0, 0, 0, 0, time.UTC), expected: 1, }, { name: "negative duration", start: time.Date(2028, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), expected: -2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dur := Diff(tt.start, tt.end) result := dur.Years() if result != tt.expected { t.Errorf("Years() = %d, want %d", result, tt.expected) } }) } } func TestDurationMonthsFloat(t *testing.T) { tests := []struct { name string start time.Time end time.Time minExpect float64 maxExpect float64 }{ { name: "1 month exactly", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), minExpect: 1.0, maxExpect: 1.0, }, { name: "1.5 months approximately", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 2, 16, 0, 0, 0, 0, time.UTC), minExpect: 1.4, maxExpect: 1.6, }, { name: "2.5 years approximately", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2028, 7, 1, 0, 0, 0, 0, time.UTC), minExpect: 30.0, maxExpect: 30.1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dur := Diff(tt.start, tt.end) result := dur.MonthsFloat() if result < tt.minExpect || result > tt.maxExpect { t.Errorf("MonthsFloat() = %f, want between %f and %f", result, tt.minExpect, tt.maxExpect) } }) } } func TestDurationYearsFloat(t *testing.T) { tests := []struct { name string start time.Time end time.Time minExpect float64 maxExpect float64 }{ { name: "1 year exactly", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), minExpect: 1.0, maxExpect: 1.0, }, { name: "1.5 years approximately", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2027, 7, 1, 0, 0, 0, 0, time.UTC), minExpect: 1.49, maxExpect: 1.51, }, { name: "2.5 years approximately", start: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2028, 7, 1, 0, 0, 0, 0, time.UTC), minExpect: 2.49, maxExpect: 2.51, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dur := Diff(tt.start, tt.end) result := dur.YearsFloat() if result < tt.minExpect || result > tt.maxExpect { t.Errorf("YearsFloat() = %f, want between %f and %f", result, tt.minExpect, tt.maxExpect) } }) } } // TestDurationNegative verifies negative duration handling func TestDurationNegative(t *testing.T) { start := time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) dur := Diff(start, end) if dur.Seconds() >= 0 { t.Error("Seconds() should be negative") } if dur.Days() >= 0 { t.Error("Days() should be negative") } if dur.Months() >= 0 { t.Error("Months() should be negative") } if dur.Years() >= 0 { t.Error("Years() should be negative") } if dur.MonthsFloat() >= 0 { t.Error("MonthsFloat() should be negative") } if dur.YearsFloat() >= 0 { t.Error("YearsFloat() should be negative") } } // TestDurationCrossBoundaries tests calculations across year boundaries func TestDurationCrossBoundaries(t *testing.T) { tests := []struct { name string start time.Time end time.Time months int years int }{ { name: "cross one year boundary", start: time.Date(2025, 11, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), months: 3, years: 0, }, { name: "cross two year boundaries", start: time.Date(2025, 11, 1, 0, 0, 0, 0, time.UTC), end: time.Date(2027, 2, 1, 0, 0, 0, 0, time.UTC), months: 15, years: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dur := Diff(tt.start, tt.end) if dur.Months() != tt.months { t.Errorf("Months() = %d, want %d", dur.Months(), tt.months) } if dur.Years() != tt.years { t.Errorf("Years() = %d, want %d", dur.Years(), tt.years) } }) } } // BenchmarkDurationSeconds benchmarks Seconds() func BenchmarkDurationSeconds(b *testing.B) { start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC) dur := Diff(start, end) b.ResetTimer() for i := 0; i < b.N; i++ { _ = dur.Seconds() } } // BenchmarkDurationDays benchmarks Days() func BenchmarkDurationDays(b *testing.B) { start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC) dur := Diff(start, end) b.ResetTimer() for i := 0; i < b.N; i++ { _ = dur.Days() } } // BenchmarkDurationMonths benchmarks Months() func BenchmarkDurationMonths(b *testing.B) { start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC) dur := Diff(start, end) b.ResetTimer() for i := 0; i < b.N; i++ { _ = dur.Months() } } // BenchmarkDurationMonthsFloat benchmarks MonthsFloat() func BenchmarkDurationMonthsFloat(b *testing.B) { start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC) dur := Diff(start, end) b.ResetTimer() for i := 0; i < b.N; i++ { _ = dur.MonthsFloat() } } // TestFloatPrecision verifies that float methods provide better precision func TestFloatPrecision(t *testing.T) { start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2026, 2, 16, 0, 0, 0, 0, time.UTC) dur := Diff(start, end) intMonths := dur.Months() floatMonths := dur.MonthsFloat() // Integer should be 1 if intMonths != 1 { t.Errorf("Months() = %d, want 1", intMonths) } // Float should be more than 1 (around 1.5) if floatMonths <= 1.0 || floatMonths >= 2.0 { t.Errorf("MonthsFloat() = %f, expected between 1.0 and 2.0", floatMonths) } // Float should have fractional part if floatMonths == math.Floor(floatMonths) { t.Error("MonthsFloat() should have fractional part") } }