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
This commit is contained in:
parent
273e920c1c
commit
e5ece8d480
4 changed files with 771 additions and 2 deletions
157
diff.go
Normal file
157
diff.go
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue