quando/diff.go
Oliver Jakoubek e5ece8d480 feat(quando-ljj): implement Duration type and Diff calculation
- Add Duration type with private start/end time fields
- Implement Diff(a, b) package function
- Implement integer methods (Seconds, Minutes, Hours, Days, Weeks, Months, Years)
- Implement float methods (MonthsFloat, YearsFloat) for precise calculations
- Months calculation handles month-end dates and leap years correctly
- Negative duration support (when a < b)
- Correct handling of year boundaries and leap years
- Comprehensive table-driven tests covering edge cases
- Negative duration tests
- Cross-boundary tests (year, leap year)
- Float precision tests
- Performance benchmarks (all meet targets)
- 98.6% test coverage (exceeds 95% requirement)
- Godoc comments with precision explanation

Benchmark results:
- Seconds: ~15ns (< 1µs target) ✓
- Months: ~54ns (< 1µs target) ✓
- MonthsFloat: ~172ns (< 2µs target) ✓
- Zero allocations for all operations

All acceptance criteria met:
✓ Duration type defined
✓ Diff(a, b) returns Duration
✓ All integer methods implemented (Seconds through Years)
✓ Float methods for Months and Years implemented
✓ Negative differences handled correctly
✓ Calculations correct across year boundaries
✓ Leap year handling correct
✓ Unit tests with 98.6% coverage
✓ Table-driven tests for various date ranges
✓ Benchmarks meet targets
✓ Godoc comments with precision explanation
2026-02-11 17:38:22 +01:00

157 lines
3.9 KiB
Go

package quando
import "time"
// Duration represents the difference between two dates.
// It provides methods to extract the duration in various units.
//
// Integer methods (Seconds, Minutes, Hours, Days, Weeks, Months, Years)
// return rounded-down values. For more precise calculations involving
// months and years, use MonthsFloat() and YearsFloat().
//
// Durations can be negative if the start date is after the end date.
type Duration struct {
start time.Time
end time.Time
}
// Diff calculates the duration between two dates.
// If a is before b, the duration is positive. If a is after b, the duration is negative.
//
// Example:
//
// start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// end := time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC)
// duration := quando.Diff(start, end)
// months := duration.Months() // 11 months
func Diff(a, b time.Time) Duration {
return Duration{
start: a,
end: b,
}
}
// Seconds returns the number of seconds in the duration (rounded down).
func (d Duration) Seconds() int64 {
return int64(d.end.Sub(d.start).Seconds())
}
// Minutes returns the number of minutes in the duration (rounded down).
func (d Duration) Minutes() int64 {
return int64(d.end.Sub(d.start).Minutes())
}
// Hours returns the number of hours in the duration (rounded down).
func (d Duration) Hours() int64 {
return int64(d.end.Sub(d.start).Hours())
}
// Days returns the number of days in the duration (rounded down).
// This calculates calendar days, not 24-hour periods.
func (d Duration) Days() int {
start := d.start
end := d.end
negative := false
// Handle negative duration
if end.Before(start) {
start, end = end, start
negative = true
}
// Calculate days by counting calendar days
days := 0
for start.Before(end) {
start = start.AddDate(0, 0, 1)
days++
}
if negative {
return -days
}
return days
}
// Weeks returns the number of weeks in the duration (rounded down).
func (d Duration) Weeks() int {
return d.Days() / 7
}
// Months returns the number of months in the duration (rounded down).
// This handles month-end dates and leap years correctly.
func (d Duration) Months() int {
start := d.start
end := d.end
negative := false
// Handle negative duration
if end.Before(start) {
start, end = end, start
negative = true
}
// Calculate months based on year and month difference
years := end.Year() - start.Year()
months := int(end.Month()) - int(start.Month())
totalMonths := years*12 + months
// Adjust if end day is before start day (not a full month yet)
if end.Day() < start.Day() {
totalMonths--
}
if negative {
return -totalMonths
}
return totalMonths
}
// Years returns the number of years in the duration (rounded down).
func (d Duration) Years() int {
return d.Months() / 12
}
// MonthsFloat returns the precise number of months in the duration as a float64.
// This provides more accurate calculations than the integer Months() method.
func (d Duration) MonthsFloat() float64 {
start := d.start
end := d.end
negative := false
// Handle negative duration
if end.Before(start) {
start, end = end, start
negative = true
}
// Get integer months first
fullMonths := float64(d.Months())
if negative {
fullMonths = -fullMonths
}
// Calculate fractional month based on days
// Create a date at the same day in the month after start + full months
baseDate := start.AddDate(0, int(fullMonths), 0)
// Days remaining after full months
daysRemaining := end.Sub(baseDate).Hours() / 24
// Days in the month we're currently in
nextMonth := baseDate.AddDate(0, 1, 0)
daysInMonth := nextMonth.Sub(baseDate).Hours() / 24
fractionalMonth := daysRemaining / daysInMonth
result := fullMonths + fractionalMonth
if negative {
return -result
}
return result
}
// YearsFloat returns the precise number of years in the duration as a float64.
// This provides more accurate calculations than the integer Years() method.
func (d Duration) YearsFloat() float64 {
return d.MonthsFloat() / 12.0
}