Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

87 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

years

Everything has a time. Travel it.

A time toolkit for Go: parsing, formatting, humanizing, and navigating time-based data.

Go Reference Go Version License: MIT


years bundles the time helpers you keep rewriting: one parser that accepts Go layouts, unix timestamps, and human aliases; zero-safe formatting with canonical layouts; "3h ago" humanization; fluent in-place mutation; and a Voyager for traversing anything that has a time - like calendar-structured file trees.

t, _ := years.JustParse("2024-05-26") // ISO dates & RFC3339 out of the box
t, _ = years.JustParse("1717852417")  // unix timestamps
t, _ = years.JustParse("yesterday")   // human aliases

years.Humanize(t) // "1d ago"

Install

go get github.com/amberpixels/years

Quick Start

package main

import (
	"fmt"

	"github.com/amberpixels/years"
)

func main() {
	created, _ := years.JustParse("2024-05-26 13:45")

	fmt.Println(years.Format(created, years.LayoutHumanDate)) // "May 26, 2024"
	fmt.Println(years.Humanize(created))                      // "2y ago"

	years.Mutate(&created).TruncateToDay().SetYear(2025)
	fmt.Println(years.Format(created, years.LayoutDate)) // "2025-05-26"

	d, _ := years.ParseISODuration("PT1H2M3S")
	fmt.Println(years.FormatDurationClock(d)) // "1:02:03"
}

Parsing

The default parser understands RFC3339 timestamps, 2006-01-02 15:04[:05], plain dates, unix-second timestamps, and aliases - no setup:

t, _ := years.JustParse("2024-05-26T13:45:00Z")
t, _ = years.JustParse("1717852417")
t, _ = years.JustParse("next-week")

// Strict single-layout parsing, like time.Parse:
t, _ = years.Parse("2006-01-02", "2024-05-26")

Aliases resolve against the package clock: today, yesterday, tomorrow, this-week, last-week, next-week, last-weekend, next-weekend, this-month, last-month, next-month, this-year, last-year, next-year.

Layouts may also contain a timestamp part: U@ for unix seconds, U@000 for milliseconds, U@000000 / U@000000000 for micro/nanoseconds:

p := years.NewParser(
	years.AcceptUnixMilli(),
	years.AcceptAliases(),
	years.WithLayouts("2006", "2006-01", "2006-Jan-02"),
)

t, _ = p.Parse("logs-U@000.log", "logs-1717852417000.log") // 2024-06-08 13:13:37 UTC
t, _ = p.JustParse("2020-01")

The global parser is configurable too - SetParserDefaults replaces its options, ExtendParserDefaults appends, ResetParserDefaults restores the out-of-the-box behavior:

years.ExtendParserDefaults(years.WithLayouts("2006, Jan 2"))
t, _ = years.JustParse("2020, Dec 1")

Formatting

Canonical display layouts (so you stop re-typing the reference-time magic strings), plus zero/nil-safe helpers:

t := time.Date(2025, time.April, 30, 13, 45, 0, 0, time.UTC)

years.Format(t, years.LayoutDate)          // "2025-04-30"
years.Format(t, years.LayoutDateTime)      // "2025-04-30 13:45:00"
years.Format(t, years.LayoutDateTimeShort) // "2025-04-30 13:45"
years.Format(t, years.LayoutHuman)         // "Apr 30, 2025 13:45"
years.Format(t, years.LayoutHumanDate)     // "Apr 30, 2025"

years.Format(time.Time{}, years.LayoutDate) // "" - the zero time renders empty
years.FormatPtr(nil, years.LayoutDate)      // "" - nil-safe

Durations

Parse ISO-8601 durations (e.g. YouTube's contentDetails.duration), and render durations media-clock style or in a compact human form:

d, _ := years.ParseISODuration("PT15M30S") // 15m30s
years.FormatDurationClock(d)               // "15:30"
years.FormatDurationClock(time.Hour + 2*time.Minute + 3*time.Second) // "1:02:03"

years.HumanizeDuration(90 * time.Minute) // "1h 30m"
years.HumanizeDuration(45 * time.Second) // "45s"

Relative Time

years.Humanize(time.Now().Add(-3 * time.Hour)) // "3h ago"
years.Humanize(time.Now().Add(48 * time.Hour)) // "in 2d"

// Clock-free variant (no global clock needed; handy in tests):
years.HumanizeFrom(base, base.Add(-5*time.Minute)) // "5m ago"

Humanize reads the package clock (years.Now()), so tests can make it deterministic via years.SetStdClock.

Mutating Time

Mutate wraps a *time.Time with fluent setters and truncation helpers that modify it in place:

t := time.Date(2025, time.April, 30, 13, 45, 59, 0, time.UTC)

years.Mutate(&t).TruncateToWeek(time.Monday) // t is now 2025-04-28 00:00:00
years.Mutate(&t).SetMonth(time.December).SetDay(24).SetHour(18)

Truncation goes down to any unit (TruncateToSecond ... TruncateToYear), setters cover SetYear ... SetNanosecond.

Voyager: Navigating Time-Based Data

Anything implementing Waypoint (an identifier, a time, optional children) can be traversed with a Voyager. Strings work out of the box:

dates := []string{"2024-03-01", "2024-01-01", "2024-02-01"}

v := years.NewVoyager(years.WaypointGroupFromStrings(dates))
_ = v.Traverse(func(w years.Waypoint) {
	fmt.Println(w.Identifier()) // 2024-03-01, 2024-02-01, 2024-01-01
}, years.O_PAST()) // newest first

Non-default layouts are given to the waypoints: years.WaypointGroupFromStrings(dates, "2006, Jan 2").

Files and directories named after dates form navigable calendars. Given a tree like calendar/2024/Jan/2024-01-15.txt:

wf, err := years.NewTimeNamedWaypointFile("calendar", "2006/Jan/2006-01-02.txt")
if err != nil {
	panic(err)
}

v := years.NewVoyager(wf)
_ = v.Traverse(func(w years.Waypoint) {
	fmt.Println(w.Identifier()) // file paths, oldest first
}, years.O_FUTURE(), years.O_LEAVES_ONLY())

// Jump straight to a date (aliases work here too):
found, _ := v.Navigate("yesterday")

Traverse options: O_PAST / O_FUTURE for direction, O_LEAVES_ONLY / O_CONTAINERS_ONLY / O_ALL for node filtering, O_NON_CALENDAR to include nodes without a parsed time. For time from file metadata (modification/creation/access) instead of names, see NewWaypointFile.

Schedules

The schedule subpackage models recurring weekly time windows - working hours, quiet hours, availability:

import "github.com/amberpixels/years/schedule"

work := schedule.Schedule{
	Days:      []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday},
	StartHour: 9,
	EndHour:   17,
	Gaps:      []schedule.TimeRange{{StartHour: 12, EndHour: 13}}, // lunch break
}

work.Contains(t)         // true on a Monday at 10:30
work.SlotsForDay(t)      // [09:00-12:00, 13:00-17:00]
work.AvailableMinutes(t) // 420
work.NextMatchingDay(t)  // next scheduled day

MultiSlotSchedule covers disjoint (even cross-midnight) windows per day, and CompositeSchedule merges several schedules into one.

Week patterns

WeekPattern is a recurring wall-clock predicate - "weekends", "nights" - with no anchor in absolute time (unlike aliases: last-week resolves to one concrete week). ParsePatterns turns a human vocabulary into patterns; comma means union:

patterns, _ := schedule.ParsePatterns("weekends,mornings")

patterns.Contains(t) // true on Saturday 15:00, or on any day at 09:30

The built-in vocabulary (case-insensitive, plural-tolerant): weekday names (monday(s)...), weekend(s), workingday(s)/workday(s)/weekday(s), and the day parts of DefaultDayPartition:

part span
night 22:00-05:00 (wraps midnight)
morning 05:00-12:00
lunchtime 12:00-14:00
afternoon 14:00-17:00
evening 17:00-22:00

The partition covers the full day exactly once (enforced by Validate), so PartOf doubles as a group-by key:

schedule.DefaultDayPartition.PartOf(t).Name // "morning" | "lunchtime" | ...

A cross-midnight span belongs to the day it starts on - Sunday night includes Monday 01:00, the way humans mean it. Patterns read the wall clock of the given time as-is (no timezone field; callers own locality). Domain words and custom partitions plug in via Vocabulary:

vocab := schedule.DefaultVocabulary()
vocab.Extra = map[string]schedule.WeekPatterns{"officehours": {{
	Days:  []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday},
	Spans: []schedule.DaySlot{{Start: schedule.TimeOfDay{Hour: 9}, End: schedule.TimeOfDay{Hour: 17}}},
}}}

patterns, _ := vocab.ParsePatterns("officehours")

Feedback

years is a solo, opinionated project - but if you stumbled upon it and have ideas, questions, or bug reports, an issue is always welcome :)

License

MIT © amberpixels

About

Go API for navigating through calendar-based objects.

Topics

Resources

Stars

5 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages