quando – Intuitive Date Calculations for Go

quando is a Go library for intuitive date calculations with a fluent API. It provides a simple, chainable interface for date operations while being immutable, having zero external dependencies, and handling edge cases like month-end dates and daylight saving time transitions correctly.

What is quando?

Date calculations in Go's standard time package can be verbose and error-prone. Simple operations like "add 3 months" or "go to the last day of the month" require careful handling of edge cases. quando provides a fluent, immutable API that makes date operations more intuitive and less error-prone while maintaining full compatibility with Go's time.Time.

Why this library?

I frequently work with date calculations in business applications – subscription periods, billing cycles, report ranges, and scheduling tasks. The standard library's approach often leads to repetitive code and subtle bugs with month boundaries.

The practical trigger was my existing DatesAPI web service – a small demo project that provides formatted date strings via a simple API. When I wanted to add more advanced date operations to it, I realized I needed a solid foundation for date calculations first. Rather than building that logic directly into the service, I extracted it into a standalone library. quando was born from the need for a simple, chainable API that handles edge cases correctly and makes the code more readable.

Installation

go get code.beautifulmachines.dev/jakoubek/quando

Quick Start

package main

import (
    "fmt"
    "time"

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

func main() {
    // Start with today's date
    today := quando.From(time.Now())

    // Add 3 months and 5 days
    future := today.Add(3, quando.Months).Add(5, quando.Days)
    fmt.Printf("In 3 months and 5 days: %s\n", future.Time())

    // Subtract 2 weeks
    past := today.Sub(2, quando.Weeks)
    fmt.Printf("2 weeks ago: %s\n", past.Time())

    // Go to the last day of the month
    monthEnd := today.EndOf(quando.Months)
    fmt.Printf("End of this month: %s\n", monthEnd.Time())

    // Chain operations
    result := today.
        Add(1, quando.Months).
        StartOf(quando.Months).
        Add(14, quando.Days)
    fmt.Printf("15th of next month: %s\n", result.Time())
}

Features

Fluent API

quando provides a chainable API that makes date calculations readable and expressive:

// Traditional approach with time package
t := time.Now()
t = t.AddDate(0, 3, 0)  // Add 3 months
t = t.AddDate(0, 0, 7)  // Add 7 days

// quando approach
result := quando.From(time.Now()).
    Add(3, quando.Months).
    Add(7, quando.Days).
    Time()

Month-End Awareness

quando handles month-end dates correctly, which is crucial for business calculations:

// January 31 + 1 month = February 28/29 (last day of Feb)
jan31 := quando.From(time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC))
result := jan31.Add(1, quando.Months)
fmt.Println(result.Time()) // 2024-02-29 (leap year)

// February 29 + 1 year = February 28 (non-leap year)
feb29 := quando.From(time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC))
result = feb29.Add(1, quando.Years)
fmt.Println(result.Time()) // 2025-02-28

Immutability

All operations return new instances, making quando safe for concurrent use:

start := quando.From(time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC))
end := start.Add(3, quando.Months)

// start is unchanged
fmt.Println(start.Time())  // 2024-01-15
fmt.Println(end.Time())    // 2024-04-15

Start and End of Period

Navigate to the beginning or end of various time periods:

now := quando.Now()

// Month boundaries
monthStart := now.StartOf(quando.Months)
monthEnd := now.EndOf(quando.Months)

// Week boundaries (Monday-based)
weekStart := now.StartOf(quando.Weeks)
weekEnd := now.EndOf(quando.Weeks)

// Year boundaries
yearStart := now.StartOf(quando.Years)
yearEnd := now.EndOf(quando.Years)

// Quarter boundaries
quarterStart := now.StartOf(quando.Quarters)
quarterEnd := now.EndOf(quando.Quarters)

Day Navigation

Find the next or previous occurrence of a weekday:

now := quando.Now()

// Next Friday
nextFriday := now.Next(time.Friday)

// Previous Monday
lastMonday := now.Prev(time.Monday)

Human-Readable Differences

Calculate and display durations between dates:

start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2026, 3, 15, 0, 0, 0, 0, time.UTC)
duration := quando.Diff(start, end)

fmt.Println(duration.Human()) // "2 years, 2 months"
fmt.Println(duration.Months()) // 26
fmt.Println(duration.Days())   // 804

Parsing and Formatting

Automatic format detection and multilingual formatting:

// Automatic format detection
date, _ := quando.Parse("2026-02-09")        // ISO format
date, _ = quando.Parse("09.02.2026")          // EU format

// Relative dates via ParseRelative
date, _ = quando.ParseRelative("+3 days")     // 3 days from now
date, _ = quando.ParseRelative("-1 week")     // 1 week ago
date, _ = quando.ParseRelative("tomorrow")    // Tomorrow

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

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

Date Inspection

date := quando.Now()

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

Zero Dependencies

quando uses only Go's standard library (time package). No external dependencies means:

  • Smaller binary size
  • Faster builds
  • No dependency conflicts
  • No supply chain security concerns

Requirements

  • Go 1.22 or higher

Links

License

MIT License – see LICENSE.