Intuitive and idiomatic date calculations for Go
go
Find a file
Oliver Jakoubek f47897f3fd Change Now() and DefaultClock to use UTC instead of local time
Changed quando.Now() and DefaultClock.Now() to return dates in UTC
timezone instead of local server timezone. This aligns with industry
standards for date/time libraries and prevents server-timezone-dependent
behavior.

Changes:
- date.go: Now() uses time.Now().UTC() instead of time.Now()
- Updated documentation comments to reflect UTC default
- date_test.go: TestNow() now verifies UTC location
- clock_test.go: TestDefaultClock_Now() now verifies UTC location
- parse_test.go: TestParseRelative() verifies all results are UTC
- parse.go: Updated comment from "local timezone" to "UTC timezone"

Users needing local time can use:
- quando.From(time.Now()) for explicit local time
- quando.Now().In("Europe/Berlin") to convert to specific timezone

Closes: quando-67n
2026-02-12 16:39:01 +01:00
.beads Change Now() and DefaultClock to use UTC instead of local time 2026-02-12 16:39:01 +01:00
.gitattributes feat(quando-91w): initialize project structure and tooling 2026-02-11 16:28:14 +01:00
.gitignore feat(quando-91w): initialize project structure and tooling 2026-02-11 16:28:14 +01:00
AGENTS.md feat(quando-91w): initialize project structure and tooling 2026-02-11 16:28:14 +01:00
arithmetic.go feat(quando-b4r): implement Add and Sub arithmetic operations 2026-02-11 17:43:03 +01:00
arithmetic_test.go feat(quando-r1o): consolidate benchmarks and achieve 99.5% test coverage 2026-02-11 21:13:09 +01:00
benchmark_test.go feat(quando-r1o): consolidate benchmarks and achieve 99.5% test coverage 2026-02-11 21:13:09 +01:00
CLAUDE.md feat(quando-91w): initialize project structure and tooling 2026-02-11 16:28:14 +01:00
clock.go feat(quando-vih): implement Clock abstraction for testability 2026-02-11 16:33:23 +01:00
clock_test.go Change Now() and DefaultClock to use UTC instead of local time 2026-02-12 16:39:01 +01:00
date.go Change Now() and DefaultClock to use UTC instead of local time 2026-02-12 16:39:01 +01:00
date_test.go Change Now() and DefaultClock to use UTC instead of local time 2026-02-12 16:39:01 +01:00
diff.go feat(quando-10t): implement human-readable duration format with i18n support 2026-02-11 19:42:10 +01:00
diff_test.go feat(quando-r1o): consolidate benchmarks and achieve 99.5% test coverage 2026-02-11 21:13:09 +01:00
errors.go feat(quando-36t): implement error types and handling 2026-02-11 17:40:01 +01:00
errors_test.go feat(quando-36t): implement error types and handling 2026-02-11 17:40:01 +01:00
example_test.go feat(quando-7m5): implement MustParse convenience function 2026-02-11 20:54:32 +01:00
format.go feat(quando-95w): implement custom layout formatting with i18n support 2026-02-11 20:34:57 +01:00
format_test.go feat(quando-r1o): consolidate benchmarks and achieve 99.5% test coverage 2026-02-11 21:13:09 +01:00
go.mod chore: update module name to code.beautifulmachines.dev/jakoubek/quando 2026-02-11 17:25:25 +01:00
i18n.go Add i18n support for 15 additional languages 2026-02-12 14:59:37 +01:00
i18n_test.go Add i18n support for 15 additional languages 2026-02-12 14:59:37 +01:00
inspect.go feat(quando-5ib): implement date inspection methods 2026-02-11 20:45:53 +01:00
inspect_test.go feat(quando-r1o): consolidate benchmarks and achieve 99.5% test coverage 2026-02-11 21:13:09 +01:00
LICENSE Updated copyright in LICENSE 2026-02-12 11:09:26 +01:00
parse.go Change Now() and DefaultClock to use UTC instead of local time 2026-02-12 16:39:01 +01:00
parse_test.go Change Now() and DefaultClock to use UTC instead of local time 2026-02-12 16:39:01 +01:00
PRD.md feat(quando-91w): initialize project structure and tooling 2026-02-11 16:28:14 +01:00
quando.go feat(quando-j2s): implement core Date type and conversions 2026-02-11 16:31:21 +01:00
README.md Updated code examples in README 2026-02-12 11:54:19 +01:00
snap.go feat(quando-9sf): implement Next and Prev weekday navigation 2026-02-11 17:33:54 +01:00
snap_test.go feat(quando-r1o): consolidate benchmarks and achieve 99.5% test coverage 2026-02-11 21:13:09 +01:00
unit.go feat(quando-4bh): implement Unit type and constants 2026-02-11 16:34:42 +01:00
unit_test.go feat(quando-r1o): consolidate benchmarks and achieve 99.5% test coverage 2026-02-11 21:13:09 +01:00

quando

Mirror on GitHub Go Reference Go Report Card License: MIT

Primary repository: code.beautifulmachines.dev/jakoubek/quando · GitHub is a read-only mirror.

Intuitive and idiomatic date calculations for Go

quando is a standalone Go library for complex date operations that are cumbersome or impossible with the Go standard library alone. It provides a fluent API for date arithmetic, parsing, formatting, and timezone-aware calculations.

Features

  • Fluent API: Chain operations naturally: quando.Now().Add(2, quando.Months).StartOf(quando.Weeks)
  • Month-End Aware: Handles edge cases like Jan 31 + 1 month = Feb 28
  • DST Safe: Calendar-based arithmetic (not clock-based)
  • Zero Dependencies: Only Go stdlib
  • Immutable: Thread-safe by design
  • i18n Ready: Multilingual formatting (EN, DE in Phase 1)
  • Testable: Built-in Clock abstraction for deterministic tests

Installation

go get code.beautifulmachines.dev/jakoubek/quando

Quick Start

import "code.beautifulmachines.dev/jakoubek/quando"

// Get current date
now := quando.Now()

// Date arithmetic with month-end handling
date := quando.From(time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC))
result := date.Add(1, quando.Months) // Feb 28, 2026 (not overflow)

// Snap to boundaries
monday := quando.Now().StartOf(quando.Weeks)     // This week's Monday 00:00
endOfMonth := quando.Now().EndOf(quando.Months)  // Last day of month 23:59:59

// Next/Previous dates
nextFriday := quando.Now().Next(time.Friday)
prevMonday := quando.Now().Prev(time.Monday)

// Human-readable differences
start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2028, 3, 15, 0, 0, 0, 0, time.UTC)
duration := quando.Diff(start, end)
fmt.Println(duration.Human()) // "2 years, 2 months"

// Parsing (automatic format detection)
date, err := quando.Parse("2026-02-09")
date, err := quando.Parse("09.02.2026")           // EU format

// Relative dates
date, err := quando.ParseRelative("+3 days")      // 3 days from now
date, err = quando.ParseRelative("-1 week")        // 1 week ago

// Formatting
date.Format(quando.ISO)      // "2026-02-09"
date.Format(quando.Long)     // "February 9, 2026"
date.FormatLayout("02 Jan")  // "09 Feb"

// Timezone conversion
utc := date.InTimezone("UTC")
berlin := date.InTimezone("Europe/Berlin")

// Date inspection
week := date.WeekNumber()       // ISO 8601 week number
quarter := date.Quarter()       // 1-4
dayOfYear := date.DayOfYear()   // 1-366

// Complex chaining for real-world scenarios
reportDeadline := quando.Now().
    Add(1, quando.Quarters).     // Next quarter
    EndOf(quando.Quarters).      // Last day of that quarter
    StartOf(quando.Weeks).       // Monday of that week
    Add(-1, quando.Weeks)        // One week before

// Multilingual formatting
dateEN := quando.Now().WithLang(quando.EN)
dateDE := quando.Now().WithLang(quando.DE)
fmt.Println(dateEN.Format(quando.Long)) // "February 9, 2026"
fmt.Println(dateDE.Format(quando.Long)) // "9. Februar 2026"

Core Concepts

Month-End Overflow Handling

When adding months, if the target day doesn't exist, quando snaps to the last valid day:

jan31 := quando.From(time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC))
feb28 := jan31.Add(1, quando.Months) // Feb 28, not March 3

DST-Aware Arithmetic

Adding days means "same time on next calendar day", not "24 hours later":

// During DST transition (23-hour day)
date := quando.From(time.Date(2026, 3, 31, 2, 0, 0, 0, cetLocation))
next := date.Add(1, quando.Days) // April 1, 2:00 CEST (not 3:00)

Immutability

All operations return new instances. Original values are never modified:

original := quando.Now()
modified := original.Add(1, quando.Days)
// original is unchanged

Why quando? Comparison to time.Time

When to Use quando

Use quando when you need:

  • Month-aware arithmetic: Add(1, Months) handles month-end overflow
  • Business logic: "Next Friday", "End of Quarter", ISO week numbers
  • Human-readable durations: "2 years, 3 months, 5 days"
  • Fluent API: Method chaining for complex date calculations
  • Automatic parsing: Detect ISO, EU, US formats automatically
  • i18n formatting: Multilingual date formatting (EN, DE, more coming)

Use time.Time when you need:

  • Simple clock arithmetic (add 24 hours)
  • High-precision timestamps (nanoseconds matter)
  • Minimal dependencies (quando is stdlib-only but adds abstraction)
  • Low-level system operations

Side-by-Side Comparison

Month Arithmetic with Overflow

// stdlib: Complex and error-prone
t := time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC)
// Add 1 month manually - need to handle overflow
nextMonth := t.AddDate(0, 1, 0) // March 3! ❌ Unexpected

// quando: Intuitive and correct
date := quando.From(time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC))
nextMonth := date.Add(1, quando.Months) // Feb 28 ✅ Expected

Finding "Next Friday"

// stdlib: Manual calculation required
t := time.Now()
daysUntilFriday := (int(time.Friday) - int(t.Weekday()) + 7) % 7
if daysUntilFriday == 0 {
    daysUntilFriday = 7 // Never return today
}
nextFriday := t.AddDate(0, 0, daysUntilFriday)

// quando: One method call
nextFriday := quando.Now().Next(time.Friday)

Human-Readable Duration

// stdlib: No built-in solution, must implement yourself
start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2028, 3, 15, 0, 0, 0, 0, time.UTC)
duration := end.Sub(start)
// duration is just time.Duration (2y3m15d shown as "19480h0m0s") ❌

// quando: Built-in human formatting
duration := quando.Diff(start, end)
fmt.Println(duration.Human()) // "2 years, 2 months" ✅

Start of Week (Monday)

// stdlib: Manual calculation
t := time.Now()
weekday := int(t.Weekday())
if weekday == 0 { // Sunday
    weekday = 7
}
daysToMonday := weekday - 1
startOfWeek := t.AddDate(0, 0, -daysToMonday)
startOfWeek = time.Date(startOfWeek.Year(), startOfWeek.Month(),
    startOfWeek.Day(), 0, 0, 0, 0, startOfWeek.Location())

// quando: One method call
startOfWeek := quando.Now().StartOf(quando.Weeks)

Feature Comparison

Feature time.Time quando
Basic date/time
Add/subtract duration
Add/subtract months (overflow-safe)
Snap to start/end of period
Next/Previous weekday
ISO 8601 week number
Quarter calculation
Human-readable duration
Automatic format parsing
Relative parsing ("tomorrow")
i18n formatting
Fluent API / chaining
Immutability guarantee ⚠️ (manual)
Testing (Clock abstraction)

Testing

quando provides a Clock interface for deterministic tests:

// In production
date := quando.Now()

// In tests
fixedTime := time.Date(2026, 2, 9, 12, 0, 0, 0, time.UTC)
clock := quando.NewFixedClock(fixedTime)
date := clock.Now() // Always returns Feb 9, 2026

Performance

quando is designed for high performance with zero allocations in hot paths:

Benchmark Results

Operation Target Actual Status
Add/Sub (Days) < 1µs 37 ns 27x faster
Add/Sub (Months) < 1µs 181 ns 5.5x faster
Diff (integer) < 1µs 51 ns 20x faster
Diff (float) < 2µs 155 ns 13x faster
Format (ISO/EU/US) < 5µs 91 ns 55x faster
Format (Long, i18n) < 10µs 267 ns 37x faster
Parse (automatic) < 10µs 106 ns 94x faster
Parse (relative) < 20µs 581 ns 34x faster

Key Performance Features:

  • Zero allocations for arithmetic operations (Add, Sub)
  • Zero allocations for snap operations (StartOf, EndOf, Next, Prev)
  • Zero allocations for date inspection (WeekNumber, Quarter, etc.)
  • Minimal allocations for formatting (1-3 per operation)
  • Immutable design enables safe concurrent use

Run benchmarks:

go test -bench=. -benchmem

Requirements

  • Go 1.22 or later
  • No external dependencies

Documentation

Full documentation available at: pkg.go.dev/code.beautifulmachines.dev/jakoubek/quando

License

MIT License - see LICENSE file for details.

Contributing

Contributions welcome! Please ensure:

  • Tests pass (go test ./...)
  • Code is formatted (go fmt ./...)
  • No vet warnings (go vet ./...)
  • Coverage ≥95% for new code

Roadmap

Phase 1 COMPLETE

  • Project setup
  • Core date operations (Add, Sub, StartOf, EndOf, Next, Prev, Diff)
  • Parsing and formatting (ISO, EU, US, Long, RFC2822, relative)
  • Timezone handling (IANA database support)
  • i18n (EN, DE)
  • Comprehensive test suite (99.5% coverage)
  • Complete documentation and examples

Phase 2 (Planned)

  • Date ranges and series
  • Batch operations
  • Performance optimizations

Phase 3

  • Holiday calendars
  • Business day calculations
  • Extended language support

Acknowledgments

Inspired by Moment.js, Carbon, and date-fns, but designed to be idiomatic Go.