Files
timefmt/README.md
2026-02-08 16:10:41 +11:00

11 KiB

timefmt

Go Reference Go Report Card

A Go library for building and converting time format strings with human-readable descriptions. timefmt provides a fluent builder API for constructing time formats and can convert between Go's reference time format, LDML tokens, and plain English descriptions.

Features

  • 🏗️ Builder Pattern: Fluent, chainable API for constructing time formats
  • 🔄 Multi-Format Output: Convert to Go format, LDML tokens, or English descriptions
  • 📦 Pre-built Formats: Common formats like ISO8601, RFC3339, etc. ready to use
  • 🔍 Format Parser: Parse existing Go time format strings
  • 📝 Type-Safe: No string concatenation errors
  • Well-Tested: Comprehensive test coverage

Installation

go get git.haelnorr.com/h/timefmt

Quick Start

package main

import (
    "fmt"
    "time"
    "git.haelnorr.com/h/timefmt"
)

func main() {
    // Build a format using the fluent API
    format := timefmt.NewBuilder().
        Year4().Dash().
        MonthNumeric2().Dash().
        DayNumeric2().Space().
        Hour24().Colon().
        Minute().Colon().
        Second().
        Build()

    // Get different representations
    fmt.Println(format.GoFormat())      // "2006-01-02 15:04:05"
    fmt.Println(format.LDML())          // "yyyy-MM-dd HH:mm:ss"
    fmt.Println(format.Description())   // "Year (4-digit), dash, Month (2-digit), ..."

    // Use it to format times
    now := time.Now()
    fmt.Println(format.Format(now))     // "2026-02-08 15:04:05"
}

Usage

Building Formats

The builder provides methods for all time components:

format := timefmt.NewBuilder().
    WeekdayFull().Comma().      // "Monday, "
    MonthFull().Space().        // "January "
    DayNumeric().Comma().       // "8, "
    Year4().Space().            // "2006 "
    Literal("at").Space().      // "at "
    Hour12().Colon().           // "3:"
    Minute().Space().           // "04 "
    AMPM().                     // "PM"
    Build()

// Outputs: "Monday, January 8, 2006 at 3:04 PM"

Pre-built Formats

Common formats are available as constants:

// ISO 8601 / RFC 3339
fmt.Println(timefmt.ISO8601.GoFormat())     // "2006-01-02T15:04:05Z07:00"
fmt.Println(timefmt.RFC3339Nano.GoFormat()) // "2006-01-02T15:04:05.999999999Z07:00"

// Date and time
fmt.Println(timefmt.DateOnly.GoFormat())    // "2006-01-02"
fmt.Println(timefmt.TimeOnly.GoFormat())    // "15:04:05"
fmt.Println(timefmt.DateTime.GoFormat())    // "2006-01-02 15:04:05"

// Regional formats
fmt.Println(timefmt.DateUS.GoFormat())      // "01/02/2006"
fmt.Println(timefmt.DateEU.GoFormat())      // "02/01/2006"

// Kitchen and stamps
fmt.Println(timefmt.Kitchen.GoFormat())     // "3:04 PM"
fmt.Println(timefmt.StampMilli.GoFormat())  // "Jan _2 15:04:05.000"

Available pre-built formats:

  • ISO8601, RFC3339, RFC3339Nano
  • DateOnly, TimeOnly, DateTime, DateTimeWithMillis
  • DateUS, DateEU, DateTimeUS, DateTimeEU
  • Kitchen, Stamp, StampMilli, StampMicro, StampNano

Parsing Existing Go Formats

Convert Go time format strings to get LDML and descriptions:

format, err := timefmt.ParseGoFormat("02/01/2006")
if err != nil {
    log.Fatal(err)
}

fmt.Println(format.GoFormat())      // "02/01/2006"
fmt.Println(format.LDML())          // "dd/MM/yyyy"
fmt.Println(format.Description())   // "Day (2-digit), slash, Month (2-digit), slash, Year (4-digit)"

Multiple Output Formats

Every Format can output in three ways:

format := timefmt.NewBuilder().
    Year4().Dash().MonthNumeric2().Dash().DayNumeric2().
    Build()

// Go reference time format (for time.Format/Parse)
goFmt := format.GoFormat()           // "2006-01-02"

// LDML tokens (Unicode standard, used by ICU, Java, Swift, etc.)
ldml := format.LDML()                // "yyyy-MM-dd"

// Human-readable English description
desc := format.Description()         // "Year (4-digit), dash, Month (2-digit), dash, Day (2-digit)"

Format and Parse Times

Use the Format to actually format and parse times:

format := timefmt.DateTime

// Format a time
now := time.Now()
formatted := format.Format(now)      // "2026-02-08 15:04:05"

// Parse a time string
parsed, err := format.Parse("2026-02-08 15:04:05")
if err != nil {
    log.Fatal(err)
}

// Parse with location
loc, _ := time.LoadLocation("America/New_York")
parsed, err = format.ParseInLocation("2026-02-08 15:04:05", loc)

Get Example Output

See what a format looks like with example data:

format := timefmt.NewBuilder().
    MonthShort().Space().DayNumeric().Comma().Year4().
    Build()

example := format.Example()  // "Feb 8, 2026" (using reference time)

Builder API Reference

Year Methods

  • Year4() - 4-digit year (2006)
  • Year2() - 2-digit year (06)

Month Methods

  • MonthNumeric() - Numeric month without leading zero (1-12)
  • MonthNumeric2() - 2-digit month with leading zero (01-12)
  • MonthShort() - Abbreviated month name (Jan, Feb, ...)
  • MonthFull() - Full month name (January, February, ...)

Day Methods

  • DayNumeric() - Numeric day without leading zero (1-31)
  • DayNumeric2() - 2-digit day with leading zero (01-31)
  • DaySpacePadded() - Space-padded day ( 1-31)
  • DayOfYear() - 3-digit day of year (001-365)
  • DayOfYearSpacePadded() - Space-padded day of year ( 1-365)

Weekday Methods

  • WeekdayShort() - Abbreviated weekday (Mon, Tue, ...)
  • WeekdayFull() - Full weekday (Monday, Tuesday, ...)

Hour Methods

  • Hour24() - 24-hour format with leading zero (00-23)
  • Hour12() - 12-hour format without leading zero (1-12)
  • Hour12Padded() - 12-hour format with leading zero (01-12)

Minute Methods

  • Minute() - 2-digit minute with leading zero (00-59)
  • MinuteUnpadded() - Minute without leading zero (0-59)

Second Methods

  • Second() - 2-digit second with leading zero (00-59)
  • SecondUnpadded() - Second without leading zero (0-59)

Subsecond Methods

  • Millisecond() - 3-digit milliseconds (.000)
  • MillisecondTrim() - Milliseconds with trailing zeros removed (.999)
  • Microsecond() - 6-digit microseconds (.000000)
  • MicrosecondTrim() - Microseconds with trailing zeros removed (.999999)
  • Nanosecond() - 9-digit nanoseconds (.000000000)
  • NanosecondTrim() - Nanoseconds with trailing zeros removed (.999999999)

AM/PM Methods

  • AMPM() - AM/PM in uppercase
  • AMPMLower() - am/pm in lowercase

Timezone Methods

  • TimezoneOffset() - Timezone offset as ±HHMM (e.g., -0700)
  • TimezoneOffsetColon() - Timezone offset as ±HH:MM (e.g., -07:00)
  • TimezoneOffsetHourOnly() - Timezone offset hours only ±HH (e.g., -07)
  • TimezoneOffsetSeconds() - Timezone offset with seconds ±HHMMSS (e.g., -070000)
  • TimezoneOffsetColonSeconds() - Timezone offset with seconds ±HH:MM:SS (e.g., -07:00:00)
  • TimezoneISO8601() - ISO 8601 timezone (Z or ±HHMM)
  • TimezoneISO8601Colon() - ISO 8601 timezone with colon (Z or ±HH:MM)
  • TimezoneName() - Timezone abbreviation (MST, PST, etc.)

Literal Methods

  • Literal(s string) - Add custom literal text
  • Dash() - Add "-"
  • Slash() - Add "/"
  • Colon() - Add ":"
  • Space() - Add " "
  • T() - Add "T" (for ISO 8601)
  • Comma() - Add ", "
  • Period() - Add "."

Building

  • Build() - Finalize and return the Format

Fragment Constants

All format components are also available as Fragment constants for advanced use:

type Fragment struct {
    GoFormat    string  // Go's reference format
    LDML        string  // LDML token
    Description string  // Human-readable description
}

// Example fragments
timefmt.Year4Digit       // Fragment{GoFormat: "2006", LDML: "yyyy", Description: "Year (4-digit)"}
timefmt.MonthNumeric2    // Fragment{GoFormat: "01", LDML: "MM", Description: "Month (2-digit)"}
timefmt.Hour24           // Fragment{GoFormat: "15", LDML: "HH", Description: "Hour (24-hour, 2-digit)"}

You can use fragments directly with NewFormat():

format := timefmt.NewFormat(
    timefmt.Year4Digit,
    "-",
    timefmt.MonthNumeric2,
    "-",
    timefmt.DayNumeric2,
)

Examples

User-Facing Format Display

Perfect for showing users what format is expected:

format := timefmt.ParseGoFormat(userConfigFormat)

// Show LDML tokens (familiar to developers)
fmt.Printf("Format: %s\n", format.LDML())
// Output: "Format: yyyy-MM-dd HH:mm:ss"

// Show description (clear for everyone)
fmt.Printf("Expected format: %s\n", format.Description())
// Output: "Expected format: Year (4-digit), dash, Month (2-digit), ..."

// Show example
fmt.Printf("Example: %s\n", format.Example())
// Output: "Example: 2026-02-08 15:04:05"

Configuration Documentation

Generate documentation for time format configuration:

formats := map[string]*timefmt.Format{
    "log_timestamp": timefmt.DateTime,
    "api_response":  timefmt.RFC3339,
    "display_date":  timefmt.DateUS,
}

for name, format := range formats {
    fmt.Printf("%s:\n", name)
    fmt.Printf("  Go format: %s\n", format.GoFormat())
    fmt.Printf("  LDML: %s\n", format.LDML())
    fmt.Printf("  Description: %s\n", format.Description())
    fmt.Printf("  Example: %s\n\n", format.Example())
}

Form Validation Messages

format := timefmt.DateOnly

err := fmt.Errorf(
    "Invalid date format. Expected format: %s (example: %s)",
    format.Description(),
    format.Example(),
)
// Error: Invalid date format. Expected format: Year (4-digit), dash, Month (2-digit), dash, Day (2-digit) (example: 2026-02-08)

LDML Token Compatibility

The LDML output follows the Unicode LDML standard, making it compatible with:

  • ICU (International Components for Unicode)
  • Java (SimpleDateFormat, DateTimeFormatter)
  • Swift (DateFormatter)
  • Moment.js and Day.js
  • date-fns
  • Most modern date/time libraries

Why timefmt?

Go's time formatting using reference times is unique and powerful, but:

  1. Not immediately intuitive - New developers need to memorize the reference time
  2. Hard to document - "2006-01-02" doesn't clearly communicate "YYYY-MM-DD"
  3. No validation helpers - No easy way to show users what format is expected

timefmt solves these problems by providing:

  • A type-safe builder that prevents format errors
  • Automatic conversion to widely-understood LDML tokens
  • Plain English descriptions for end users
  • Pre-built formats for common use cases

License

MIT License - see LICENSE file for details

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.