11 KiB
timefmt
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,RFC3339NanoDateOnly,TimeOnly,DateTime,DateTimeWithMillisDateUS,DateEU,DateTimeUS,DateTimeEUKitchen,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 uppercaseAMPMLower()- 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 textDash()- 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:
- Not immediately intuitive - New developers need to memorize the reference time
- Hard to document - "2006-01-02" doesn't clearly communicate "YYYY-MM-DD"
- 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.