quando/inspect.go
Oliver Jakoubek a1d87c40c0 feat(quando-5ib): implement date inspection methods
Implement comprehensive date inspection API for querying metadata:

Individual methods:
- WeekNumber() - ISO 8601 week numbers (1-53)
- Quarter() - Fiscal quarters (1-4)
- DayOfYear() - Julian day (1-366)
- IsWeekend() - Weekend detection (Sat/Sun)
- IsLeapYear() - Leap year detection (4/100/400 rule)

Aggregated method:
- Info() - Returns DateInfo struct with all metadata + Unix timestamp

Implementation details:
- Delegates to stdlib: WeekNumber uses time.ISOWeek(), DayOfYear uses time.YearDay()
- Zero allocations for all operations
- Performance: All methods < 60ns (far exceeds <1µs target)
- ISO 8601 compliant week numbers (Week 1 contains Thursday)
- Handles edge cases: year boundaries, week 53, century leap years

Testing:
- 83 comprehensive test cases covering all methods
- Edge cases: year boundaries, week 53, leap years, century rules
- 6 benchmarks (all <1µs, zero allocations)
- 8 example tests with clear documentation
- Coverage: 97.7%

Files:
- inspect.go: 145 lines (6 methods + DateInfo struct)
- inspect_test.go: 464 lines (tests + benchmarks)
- example_test.go: +124 lines (8 examples)
2026-02-11 20:45:53 +01:00

150 lines
4.1 KiB
Go

package quando
import "time"
// DateInfo contains aggregated metadata about a date.
// Returned by the Info() method.
type DateInfo struct {
WeekNumber int // ISO 8601 week number (1-53)
Quarter int // Fiscal quarter (1-4)
DayOfYear int // Day of year (1-366)
IsWeekend bool // True if Saturday or Sunday
IsLeapYear bool // True if date is in a leap year
Unix int64 // Unix timestamp
}
// WeekNumber returns the ISO 8601 week number (1-53).
//
// ISO 8601 definition:
// - Week 1 is the first week with a Thursday in it
// - Weeks start on Monday and end on Sunday
// - Dates in early January may belong to week 52/53 of previous year
// - Dates in late December may belong to week 1 of next year
//
// Examples:
// - Jan 1, 2026 (Thursday) is in week 1 of 2026
// - Jan 1, 2025 (Wednesday) is in week 1 of 2025
// - Jan 1, 2024 (Monday) is in week 1 of 2024
// - Jan 1, 2023 (Sunday) is in week 52 of 2022
//
// Performance: < 1 µs, zero allocations
//
// Example:
//
// date := quando.From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC))
// week := date.WeekNumber() // 7
func (d Date) WeekNumber() int {
_, week := d.t.ISOWeek()
return week
}
// Quarter returns the fiscal quarter (1-4) for the date.
//
// Quarter mapping:
// - Q1: January, February, March
// - Q2: April, May, June
// - Q3: July, August, September
// - Q4: October, November, December
//
// Performance: < 1 µs, zero allocations
//
// Example:
//
// date := quando.From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC))
// q := date.Quarter() // 1 (Q1)
func (d Date) Quarter() int {
month := d.t.Month()
switch {
case month >= 1 && month <= 3:
return 1
case month >= 4 && month <= 6:
return 2
case month >= 7 && month <= 9:
return 3
default: // month >= 10 && month <= 12
return 4
}
}
// DayOfYear returns the day of the year (1-366).
// Also known as "ordinal date" or "Julian day number".
//
// January 1 = 1, December 31 = 365 (or 366 in leap years)
//
// Performance: < 1 µs, zero allocations
//
// Example:
//
// date := quando.From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC))
// day := date.DayOfYear() // 40 (Feb 9)
func (d Date) DayOfYear() int {
return d.t.YearDay()
}
// IsWeekend returns true if the date falls on a weekend (Saturday or Sunday).
//
// Note: This uses the ISO convention where Saturday and Sunday are considered
// weekend days. This is not configurable in Phase 1.
//
// Performance: < 1 µs, zero allocations
//
// Example:
//
// date := quando.From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)) // Monday
// isWeekend := date.IsWeekend() // false
func (d Date) IsWeekend() bool {
weekday := d.t.Weekday()
return weekday == time.Saturday || weekday == time.Sunday
}
// IsLeapYear returns true if the date's year is a leap year.
//
// Leap year rules:
// - Divisible by 4: leap year (e.g., 2024)
// - EXCEPT divisible by 100: not a leap year (e.g., 1900, 2100)
// - EXCEPT divisible by 400: leap year (e.g., 2000, 2400)
//
// Performance: < 1 µs, zero allocations
//
// Example:
//
// date := quando.From(time.Date(2024, 2, 9, 0, 0, 0, 0, time.UTC))
// isLeap := date.IsLeapYear() // true (2024 is a leap year)
func (d Date) IsLeapYear() bool {
year := d.t.Year()
// Apply leap year rules
if year%400 == 0 {
return true // Divisible by 400: leap year
}
if year%100 == 0 {
return false // Divisible by 100 but not 400: not leap year
}
if year%4 == 0 {
return true // Divisible by 4 but not 100: leap year
}
return false // Not divisible by 4: not leap year
}
// Info returns aggregated metadata about the date.
//
// This is a convenience method that calls all inspection methods
// and packages the results into a single struct.
//
// Performance: < 1 µs (sum of all individual methods)
//
// Example:
//
// date := quando.From(time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC))
// info := date.Info()
// fmt.Printf("Week: %d, Quarter: %d\n", info.WeekNumber, info.Quarter)
func (d Date) Info() DateInfo {
return DateInfo{
WeekNumber: d.WeekNumber(),
Quarter: d.Quarter(),
DayOfYear: d.DayOfYear(),
IsWeekend: d.IsWeekend(),
IsLeapYear: d.IsLeapYear(),
Unix: d.Unix(),
}
}