Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 85 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ A Go implementation inspired by Java's `java.time` package (JSR-310), providing
- 📅 **LocalDate**: Date without time (e.g., `2024-03-15`)
- ⏰ **LocalTime**: Time without date (e.g., `14:30:45.123456789`)
- 📆 **LocalDateTime**: Date-time without timezone (e.g., `2024-03-15T14:30:45.123456789`)
- 🌐 **ZoneOffset**: Time-zone offset from Greenwich/UTC (e.g., `+08:00`)
- 🌐 **ZoneOffset**: Time-zone offset from Greenwich/UTC (e.g., `+08:00`, `-05:00`, `Z`)
- 🌍 **OffsetDateTime**: Date-time with offset (e.g., `2024-03-15T14:30:45.123456789+01:00`)
- 🔢 **Field**: Enumeration of date-time fields (like Java's `ChronoField`)
- 🔍 **TemporalAccessor**: Universal interface for querying temporal objects
Expand All @@ -32,7 +32,7 @@ A Go implementation inspired by Java's `java.time` package (JSR-310), providing
- ✅ **Date arithmetic**: Add/subtract days, months, years with overflow handling
- ✅ **Type-safe field access**: Query any field with `TemporalValue` return type that validates support and overflow
- ✅ **TemporalAccessor interface**: Universal query pattern across all temporal types
- ✅ **Zero-copy text marshaling** with `encoding.TextAppender`
- ✅ **Chain operations**: Fluent API with error handling for complex mutations
- ✅ **Immutable**: All operations return new values
- ✅ **Type-safe**: Compile-time safety with distinct types
- ✅ **Zero-value friendly**: Zero values are properly handled
Expand All @@ -57,26 +57,26 @@ import (

func main() {
// Create dates and times
date := goda.MustNewLocalDate(2024, goda.March, 15)
time := goda.MustNewLocalTime(14, 30, 45, 123456789)
date := goda.MustLocalDateOf(2024, goda.March, 15)
time := goda.MustLocalTimeOf(14, 30, 45, 123456789)
datetime := date.AtTime(time) // or time.AtDate(date)

fmt.Println(date) // 2024-03-15
fmt.Println(time) // 14:30:45.123456789
fmt.Println(datetime) // 2024-03-15T14:30:45.123456789

// Create from components directly
datetime2 := goda.MustNewLocalDateTime(2024, goda.March, 15, 14, 30, 45, 123456789)
datetime2 := goda.MustLocalDateTimeOf(2024, goda.March, 15, 14, 30, 45, 123456789)

// With time zone offset
offset := goda.MustZoneOffsetOfHours(1) // +01:00
offsetDateTime := datetime.AtOffset(offset)
fmt.Println(offsetDateTime) // 2024-03-15T14:30:45.123456789+01:00

// Parse from strings
date, _ = goda.ParseLocalDate("2024-03-15")
time = goda.MustParseLocalTime("14:30:45.123456789")
datetime = goda.MustParseLocalDateTime("2024-03-15T14:30:45")
date, _ = goda.LocalDateParse("2024-03-15")
time = goda.MustLocalTimeParse("14:30:45.123456789")
datetime = goda.MustLocalDateTimeParse("2024-03-15T14:30:45")

// Get current date/time
today := goda.LocalDateNow()
Expand All @@ -85,9 +85,9 @@ func main() {
currentOffsetDateTime := goda.OffsetDateTimeNow()

// Date arithmetic
tomorrow := today.PlusDays(1)
nextMonth := today.PlusMonths(1)
nextYear := today.PlusYears(1)
tomorrow := today.Chain().PlusDays(1).MustGet()
nextMonth := today.Chain().PlusMonths(1).MustGet()
nextYear := today.Chain().PlusYears(1).MustGet()

// Comparisons
if tomorrow.IsAfter(today) {
Expand All @@ -96,79 +96,42 @@ func main() {
}
```

### Working with Time Zones

```go
// Create with offset
offset := goda.MustZoneOffsetOfHours(8) // +08:00 (China Standard Time)
odt := goda.MustNewOffsetDateTime(2024, goda.March, 15, 14, 30, 45, 0, offset)

// Parse with offset
odt, _ = goda.ParseOffsetDateTime("2024-03-15T14:30:45+08:00")
odt = goda.MustParseOffsetDateTime("2024-03-15T14:30:45Z") // UTC

// Convert from Go's time.Time (preserves offset)
goTime := time.Now()
odt = goda.OffsetDateTimeOfGoTime(goTime)

// Change offset while keeping local time
est := goda.MustZoneOffsetOfHours(-5) // EST
pst := goda.MustZoneOffsetOfHours(-8) // PST
odtEST := goda.MustNewOffsetDateTime(2024, goda.March, 15, 14, 30, 45, 0, est)
odtPST := odtEST.WithOffsetSameLocal(pst) // Local time unchanged: 14:30:45-08:00

// Change offset while keeping the instant
odtPST2 := odtEST.WithOffsetSameInstant(pst) // Instant preserved: 11:30:45-08:00

// Time arithmetic with offset
tomorrow := odt.PlusDays(1)
inTwoHours := odt.PlusHours(2)

// Convert to Unix timestamp
epochSecond := odt.ToEpochSecond()

// Compare based on instant
if odt1.IsBefore(odt2) {
fmt.Println("odt1 is earlier")
}
```

### Field Access with TemporalValue

Access individual date-time fields using the `Field` enumeration with type-safe `TemporalValue` returns:

```go
date := goda.MustNewLocalDate(2024, goda.March, 15)
date := goda.MustLocalDateOf(2024, goda.March, 15)

// Check field support
fmt.Println(date.IsSupportedField(goda.DayOfMonth)) // true
fmt.Println(date.IsSupportedField(goda.HourOfDay)) // false
fmt.Println(date.IsSupportedField(goda.FieldDayOfMonth)) // true
fmt.Println(date.IsSupportedField(goda.FieldHourOfDay)) // false

// Get field values with validation
year := date.GetField(goda.YearField)
year := date.GetField(goda.FieldYear)
if year.Valid() {
fmt.Println("Year:", year.Int64()) // 2024
}

dayOfWeek := date.GetField(goda.DayOfWeekField)
dayOfWeek := date.GetField(goda.FieldDayOfWeek)
if dayOfWeek.Valid() {
fmt.Println("Day of week:", dayOfWeek.Int()) // 5 (Friday)
}

// Unsupported fields return unsupported TemporalValue
hourOfDay := date.GetField(goda.HourOfDay)
hourOfDay := date.GetField(goda.FieldHourOfDay)
if hourOfDay.Unsupported() {
fmt.Println("Hour field is not supported for LocalDate")
}

// Time fields
time := goda.MustNewLocalTime(14, 30, 45, 123456789)
hour := time.GetField(goda.HourOfDay)
time := goda.MustLocalTimeOf(14, 30, 45, 123456789)
hour := time.GetField(goda.FieldHourOfDay)
if hour.Valid() {
fmt.Println("Hour:", hour.Int()) // 14
}

nanoOfDay := time.GetField(goda.NanoOfDay)
nanoOfDay := time.GetField(goda.FieldNanoOfDay)
if nanoOfDay.Valid() {
fmt.Println("Nanoseconds since midnight:", nanoOfDay.Int64())
}
Expand Down Expand Up @@ -203,7 +166,7 @@ type TemporalAccessor interface {

// Write generic functions that work with any temporal type
func printYear(t goda.TemporalAccessor) {
if year := t.GetField(goda.YearField); year.Valid() {
if year := t.GetField(goda.FieldYear); year.Valid() {
fmt.Printf("Year: %d\n", year.Int())
}
}
Expand All @@ -213,6 +176,41 @@ printYear(goda.LocalDateNow())
printYear(goda.LocalDateTimeNow())
```

### Chain Operations

All temporal types support chain operations for fluent, error-handled mutations. Chain operations allow you to perform multiple modifications in a single expression with proper error handling:

```go
// Chain multiple operations fluently
dt := goda.MustLocalDateTimeOf(2024, goda.March, 15, 14, 30, 45, 123456789)

// Chain date and time modifications
meetingTime := dt.Chain().
PlusDays(7). // Next week
WithHour(16). // At 4 PM
WithMinute(0). // On the hour
WithSecond(0). // No seconds
WithNano(0). // No nanoseconds
MustGet() // Get result (panics on error)

fmt.Println("Meeting scheduled for:", meetingTime)

// Error handling with chains
result, err := dt.Chain().
PlusMonths(1).
WithDayOfMonth(32). // Invalid day - will cause error
GetResult() // Returns (zero value, error)

if err != nil {
fmt.Println("Invalid operation:", err)
// Use fallback
validTime := dt.Chain().
PlusMonths(1).
WithDayOfMonth(31). // Valid day
GetOrElse(dt) // Fallback to original if error
}
```

### JSON Serialization

```go
Expand All @@ -226,10 +224,10 @@ type Event struct {

event := Event{
Name: "Meeting",
Date: goda.MustNewLocalDate(2024, goda.March, 15),
Time: goda.MustNewLocalTime(14, 30, 0, 0),
CreatedAt: goda.MustParseLocalDateTime("2024-03-15T14:30:00"),
ScheduledAt: goda.MustParseOffsetDateTime("2024-03-15T14:30:00+08:00"),
Date: goda.MustLocalDateOf(2024, goda.March, 15),
Time: goda.MustLocalTimeOf(14, 30, 0, 0),
CreatedAt: goda.MustLocalDateTimeParse("2024-03-15T14:30:00"),
ScheduledAt: goda.MustOffsetDateTimeParse("2024-03-15T14:30:00+08:00"),
}

jsonData, _ := json.Marshal(event)
Expand Down Expand Up @@ -276,6 +274,30 @@ db.Exec("INSERT INTO records (created_at, updated_at) VALUES (?, ?)",
| `Field` | Date-time field enumeration | `HourOfDay`, `DayOfMonth` |
| `TemporalAccessor` | Interface for querying temporal objects | Implemented by all temporal types |
| `TemporalValue` | Type-safe field value with validation | Returned by `GetField()` |
| `Error` | Structured error with context | Provides detailed error information |
| `LocalDateChain` | Chain operations for LocalDate | `date.Chain().PlusDays(1).MustGet()` |
| `LocalTimeChain` | Chain operations for LocalTime | `time.Chain().PlusHours(1).MustGet()` |
| `LocalDateTimeChain`| Chain operations for LocalDateTime | `dt.Chain().PlusDays(1).MustGet()` |
| `OffsetDateTimeChain`| Chain operations for OffsetDateTime | `odt.Chain().PlusHours(1).MustGet()` |

### Format Specification

This package uses ISO 8601 basic calendar date and time formats (not the full specification):

**LocalDate**: `yyyy-MM-dd` (e.g., "2024-03-15")
Only Gregorian calendar dates. No week dates (YYYY-Www-D) or ordinal dates (YYYY-DDD).

**LocalTime**: `HH:mm:ss[.nnnnnnnnn]` (e.g., "14:30:45.123456789")
24-hour format. Fractional seconds up to nanoseconds. Fractional seconds are aligned to 3-digit boundaries (milliseconds, microseconds, nanoseconds) for Java.time compatibility: 100ms → "14:30:45.100", 123.4ms → "14:30:45.123400". Parsing accepts any length of fractional seconds (e.g., "14:30:45.1" → 100ms).

**LocalDateTime**: `yyyy-MM-ddTHH:mm:ss[.nnnnnnnnn]` (e.g., "2024-03-15T14:30:45.123456789")
Combined with 'T' separator (lowercase 't' accepted when parsing).

**ZoneOffset**: `±HH:mm[:ss]` or `Z` for UTC (e.g., "+08:00", "-05:30", "Z")
Hours must be in range [-18, 18], minutes and seconds in [0, 59]. Compact formats (±HH, ±HHMM, ±HHMMSS) are also supported.

**OffsetDateTime**: `yyyy-MM-ddTHH:mm:ss[.nnnnnnnnn]±HH:mm[:ss]` (e.g., "2024-03-15T14:30:45+08:00")
Combines LocalDateTime and ZoneOffset. 'Z' is accepted as UTC offset.

### Time Formatting

Expand Down Expand Up @@ -304,7 +326,7 @@ All temporal types (`LocalDate`, `LocalTime`, `LocalDateTime`, `OffsetDateTime`)
- `TemporalAccessor`: Universal query interface with `GetField(field Field) TemporalValue`
- `fmt.Stringer`
- `encoding.TextMarshaler` / `encoding.TextUnmarshaler`
- `encoding.TextAppender` (zero-copy text marshaling)
- `encoding.TextAppender`
- `json.Marshaler` / `json.Unmarshaler`
- `sql.Scanner` / `driver.Valuer`

Expand Down
Loading
Loading