Intuitive and idiomatic date calculations for Go
go
Find a file
Oliver Jakoubek 2bf1df03ea feat(quando-36t): implement error types and handling
- Create errors.go with sentinel errors
- Define ErrInvalidFormat for parsing errors
- Define ErrInvalidTimezone for timezone errors
- Define ErrOverflow for date arithmetic overflow
- Comprehensive godoc for each error with usage context
- Document no-panic policy in error handling
- Document Must* variant panic behavior
- Example tests showing error handling patterns
- Tests for error uniqueness and errors.Is compatibility
- 100% coverage for error definitions

Error Handling Policy:
- Library never panics in normal operations
- All errors returned as values
- Use errors.Is() for error type checking
- Must* functions panic (for test/init use only)

All acceptance criteria met:
✓ errors.go file created
✓ ErrInvalidFormat defined
✓ ErrInvalidTimezone defined
✓ ErrOverflow defined
✓ Godoc for each error with usage context
✓ Documentation of no-panic policy
✓ Documentation of Must* panic behavior
✓ Example tests showing error handling patterns
2026-02-11 17:40:01 +01:00
.beads feat(quando-36t): implement error types and handling 2026-02-11 17:40:01 +01:00
.github/workflows feat(quando-91w): initialize project structure and tooling 2026-02-11 16:28:14 +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
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 feat(quando-vih): implement Clock abstraction for testability 2026-02-11 16:33:23 +01:00
date.go feat(quando-j2s): implement core Date type and conversions 2026-02-11 16:31:21 +01:00
date_test.go feat(quando-j2s): implement core Date type and conversions 2026-02-11 16:31:21 +01:00
diff.go feat(quando-ljj): implement Duration type and Diff calculation 2026-02-11 17:38:22 +01:00
diff_test.go feat(quando-ljj): implement Duration type and Diff calculation 2026-02-11 17:38:22 +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-36t): implement error types and handling 2026-02-11 17:40:01 +01:00
go.mod chore: update module name to code.beautifulmachines.dev/jakoubek/quando 2026-02-11 17:25:25 +01:00
LICENSE feat(quando-91w): initialize project structure and tooling 2026-02-11 16:28:14 +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 docs: update module path in README to include jakoubek namespace 2026-02-11 17:25:43 +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-9sf): implement Next and Prev weekday navigation 2026-02-11 17:33:54 +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-4bh): implement Unit type and constants 2026-02-11 16:34:42 +01:00

quando

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, Months).StartOf(Week)
  • 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.Week)     // This week's Monday 00:00
endOfMonth := quando.Now().EndOf(quando.Month)  // Last day of month 23:59:59

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

// Differences
duration := quando.Diff(startDate, endDate)
months := duration.Months()
humanReadable := duration.Human() // "2 years, 3 months, 5 days"

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

// 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

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

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

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 (Current)

  • Project setup
  • 🚧 Core date operations
  • 🚧 Parsing and formatting
  • 🚧 Timezone handling
  • 🚧 i18n (EN, DE)

Phase 2

  • 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.