# timefmt [![Go Reference](https://pkg.go.dev/badge/git.haelnorr.com/h/timefmt.svg)](https://pkg.go.dev/git.haelnorr.com/h/timefmt) [![Go Report Card](https://goreportcard.com/badge/git.haelnorr.com/h/timefmt)](https://goreportcard.com/report/git.haelnorr.com/h/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 ```bash go get git.haelnorr.com/h/timefmt ``` ## Quick Start ```go 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: ```go 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: ```go // 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: ```go 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: ```go 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: ```go 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: ```go 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: ```go 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()`: ```go format := timefmt.NewFormat( timefmt.Year4Digit, "-", timefmt.MonthNumeric2, "-", timefmt.DayNumeric2, ) ``` ## Examples ### User-Facing Format Display Perfect for showing users what format is expected: ```go 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: ```go 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 ```go 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](https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table), 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.