362 lines
11 KiB
Markdown
362 lines
11 KiB
Markdown
# timefmt
|
|
|
|
[](https://pkg.go.dev/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.
|