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

362 lines
11 KiB
Markdown

# 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.