Skip to content
This repository was archived by the owner on Jul 30, 2026. It is now read-only.

Commit 05d02fa

Browse files
committed
feat: introduce TemporalAccessor interface and TemporalValue for type-safe field queries
Replace GetFieldInt64() method with GetField() that returns TemporalValue, providing a more robust and type-safe API for querying temporal fields. Breaking Changes: - LocalDate.GetFieldInt64(field Field) int64 → GetField(field Field) TemporalValue - LocalTime.GetFieldInt64(field Field) int64 → GetField(field Field) TemporalValue - LocalDateTime.GetFieldInt64(field Field) int64 → GetField(field Field) TemporalValue New Features: - Add TemporalAccessor interface for universal temporal object querying - Add TemporalValue type with validation states (Valid/Unsupported/Overflow) - All temporal types now implement TemporalAccessor interface - Enable generic programming with temporal types via interface Benefits: - Type-safe field access with explicit validation - Clear distinction between unsupported fields and zero values - Better error handling and debugging experience - Future-proof API ready for overflow detection - Consistent query pattern across all temporal types Files Changed: - temporal_accessor.go: new interface definition - temporal_value.go: new value wrapper with validation - local_date.go: implement GetField() with TemporalValue - local_time.go: implement GetField() with TemporalValue - local_date_time.go: implement GetField() with TemporalValue - field_test.go: comprehensive tests for new API - example_test.go: updated examples demonstrating new usage - README.md: updated documentation with new API and examples
1 parent aef17e4 commit 05d02fa

8 files changed

Lines changed: 837 additions & 162 deletions

File tree

README.md

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ A Go implementation inspired by Java's `java.time` package (JSR-310), providing
1717
-**LocalTime**: Time without date (e.g., `14:30:45.123456789`)
1818
- 📆 **LocalDateTime**: Date-time (e.g., `2024-03-15T14:30:45.123456789`)
1919
- 🔢 **Field**: Enumeration of date-time fields (like Java's `ChronoField`)
20+
- 🔍 **TemporalAccessor**: Universal interface for querying temporal objects
21+
- 📊 **TemporalValue**: Type-safe wrapper for field values with validation state
2022

2123
### Key Features
2224

2325
-**ISO 8601 basic format** support (yyyy-MM-dd, HH:mm:ss[.nnnnnnnnn], combined with 'T')
2426
-**Java.time compatible formatting**: Fractional seconds aligned to 3-digit boundaries (milliseconds, microseconds, nanoseconds)
2527
-**Full JSON and SQL** database integration
2628
-**Date arithmetic**: Add/subtract days, months, years with overflow handling
27-
-**Field access**: Get any field value (year, month, hour, nano-of-day, etc.)
29+
-**Type-safe field access**: Query any field with `TemporalValue` return type that validates support and overflow
30+
-**TemporalAccessor interface**: Universal query pattern across all temporal types
2831
-**Zero-copy text marshaling** with `encoding.TextAppender`
2932
-**Immutable**: All operations return new values
3033
-**Type-safe**: Compile-time safety with distinct types
@@ -80,9 +83,9 @@ func main() {
8083
}
8184
```
8285

83-
### Field Access
86+
### Field Access with TemporalValue
8487

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

8790
```go
8891
date := goda.MustNewLocalDate(2024, goda.March, 15)
@@ -91,16 +94,73 @@ date := goda.MustNewLocalDate(2024, goda.March, 15)
9194
fmt.Println(date.IsSupportedField(goda.DayOfMonth)) // true
9295
fmt.Println(date.IsSupportedField(goda.HourOfDay)) // false
9396

94-
// Get field values
95-
year := date.GetFieldInt64(goda.YearField) // 2024
96-
dayOfWeek := date.GetFieldInt64(goda.DayOfWeekField) // 5 (Friday)
97-
dayOfYear := date.GetFieldInt64(goda.DayOfYear) // 75
98-
epochDays := date.GetFieldInt64(goda.EpochDay) // Days since Unix epoch
97+
// Get field values with validation
98+
year := date.GetField(goda.YearField)
99+
if year.Valid() {
100+
fmt.Println("Year:", year.Int64()) // 2024
101+
}
102+
103+
dayOfWeek := date.GetField(goda.DayOfWeekField)
104+
if dayOfWeek.Valid() {
105+
fmt.Println("Day of week:", dayOfWeek.Int()) // 5 (Friday)
106+
}
99107

108+
// Unsupported fields return unsupported TemporalValue
109+
hourOfDay := date.GetField(goda.HourOfDay)
110+
if hourOfDay.Unsupported() {
111+
fmt.Println("Hour field is not supported for LocalDate")
112+
}
113+
114+
// Time fields
100115
time := goda.MustNewLocalTime(14, 30, 45, 123456789)
101-
hour := time.GetFieldInt64(goda.HourOfDay) // 14
102-
nanoOfDay := time.GetFieldInt64(goda.NanoOfDay) // Total nanoseconds since midnight
103-
ampm := time.GetFieldInt64(goda.AmPmOfDay) // 1 (PM)
116+
hour := time.GetField(goda.HourOfDay)
117+
if hour.Valid() {
118+
fmt.Println("Hour:", hour.Int()) // 14
119+
}
120+
121+
nanoOfDay := time.GetField(goda.NanoOfDay)
122+
if nanoOfDay.Valid() {
123+
fmt.Println("Nanoseconds since midnight:", nanoOfDay.Int64())
124+
}
125+
```
126+
127+
**TemporalValue API:**
128+
- `Valid() bool`: Returns true if the field is supported and no overflow occurred
129+
- `Unsupported() bool`: Returns true if the field is not supported by this temporal type
130+
- `Overflow() bool`: Returns true if the field value overflowed (reserved for future use)
131+
- `Int64() int64`: Get the raw value as int64
132+
- `Int() int`: Get the value as int (for convenience)
133+
134+
**Why TemporalValue?**
135+
136+
The `TemporalValue` return type provides type-safe field queries that prevent silent errors:
137+
- **Explicit validation**: Check `Valid()` before using the value
138+
- **Clear error semantics**: Distinguish between unsupported fields and actual errors
139+
- **Future-proof**: Ready for overflow detection when needed
140+
- **No silent zeros**: Unlike raw `int64` returns, you can distinguish between "0" and "unsupported"
141+
142+
### TemporalAccessor Interface
143+
144+
All temporal types implement the `TemporalAccessor` interface, providing a uniform query pattern:
145+
146+
```go
147+
// TemporalAccessor provides read-only access to temporal fields
148+
type TemporalAccessor interface {
149+
IsZero() bool
150+
IsSupportedField(field Field) bool
151+
GetField(field Field) TemporalValue
152+
}
153+
154+
// Write generic functions that work with any temporal type
155+
func printYear(t goda.TemporalAccessor) {
156+
if year := t.GetField(goda.YearField); year.Valid() {
157+
fmt.Printf("Year: %d\n", year.Int())
158+
}
159+
}
160+
161+
// Works with LocalDate, LocalTime, or LocalDateTime
162+
printYear(goda.LocalDateNow())
163+
printYear(goda.LocalDateTimeNow())
104164
```
105165

106166
### JSON Serialization
@@ -152,6 +212,8 @@ db.QueryRow("SELECT id, created_at, date FROM records WHERE id = ?", 1).Scan(
152212
| `Year` | Year | `2024` |
153213
| `DayOfWeek` | Day of week (1=Monday, 7=Sunday) | `Friday` |
154214
| `Field` | Date-time field enumeration | `HourOfDay`, `DayOfMonth` |
215+
| `TemporalAccessor` | Interface for querying temporal objects | Implemented by all temporal types |
216+
| `TemporalValue` | Type-safe field value with validation | Returned by `GetField()` |
155217

156218
### Time Formatting
157219

@@ -176,7 +238,8 @@ Fractional seconds are automatically aligned to 3-digit boundaries (milliseconds
176238

177239
### Implemented Interfaces
178240

179-
All types implement:
241+
All temporal types (`LocalDate`, `LocalTime`, `LocalDateTime`) implement:
242+
- `TemporalAccessor`: Universal query interface with `GetField(field Field) TemporalValue`
180243
- `fmt.Stringer`
181244
- `encoding.TextMarshaler` / `encoding.TextUnmarshaler`
182245
- `encoding.TextAppender` (zero-copy text marshaling)
@@ -191,7 +254,8 @@ This package follows the **ThreeTen/JSR-310** model (Java's `java.time` package)
191254
- **Type-safe**: Distinct types for date, time, and datetime
192255
- **Simple formats**: Uses ISO 8601 basic formats (not the full complex specification)
193256
- **Database-friendly**: Direct SQL integration
194-
- **Field-based access**: Universal field access pattern via `GetFieldInt64`
257+
- **Field-based access**: Universal field access pattern via `TemporalAccessor` interface
258+
- **Safe field queries**: `TemporalValue` return type validates field support and prevents silent errors
195259
- **Zero-value safe**: Zero values are properly handled throughout
196260

197261
### When to Use LocalDate, LocalTime, LocalDateTime

example_test.go

Lines changed: 206 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -595,38 +595,222 @@ func ExampleLocalDateTime_IsSupportedField() {
595595
// Supports OffsetSeconds: false
596596
}
597597

598-
// ExampleLocalDate_GetFieldInt64 demonstrates getting field values from a date.
599-
func ExampleLocalDate_GetFieldInt64() {
598+
// ExampleLocalDate_GetField demonstrates querying date fields with TemporalValue.
599+
func ExampleLocalDate_GetField() {
600600
date := goda.MustNewLocalDate(2024, goda.March, 15) // Friday
601601

602-
fmt.Printf("Year: %d\n", date.GetFieldInt64(goda.YearField))
603-
fmt.Printf("Month: %d\n", date.GetFieldInt64(goda.MonthOfYear))
604-
fmt.Printf("Day: %d\n", date.GetFieldInt64(goda.DayOfMonth))
605-
fmt.Printf("Day of week: %d\n", date.GetFieldInt64(goda.DayOfWeekField))
606-
fmt.Printf("Day of year: %d\n", date.GetFieldInt64(goda.DayOfYear))
602+
// Query various date fields
603+
dayOfWeek := date.GetField(goda.DayOfWeekField)
604+
if dayOfWeek.Valid() {
605+
fmt.Printf("Day of week: %d (1=Monday, 7=Sunday)\n", dayOfWeek.Int())
606+
}
607+
608+
month := date.GetField(goda.MonthOfYear)
609+
if month.Valid() {
610+
fmt.Printf("Month: %d\n", month.Int())
611+
}
612+
613+
year := date.GetField(goda.YearField)
614+
if year.Valid() {
615+
fmt.Printf("Year: %d\n", year.Int())
616+
}
617+
618+
// Query unsupported field (time field on date)
619+
hour := date.GetField(goda.HourOfDay)
620+
if hour.Unsupported() {
621+
fmt.Println("Hour field is not supported for LocalDate")
622+
}
607623

608624
// Output:
609-
// Year: 2024
625+
// Day of week: 5 (1=Monday, 7=Sunday)
610626
// Month: 3
611-
// Day: 15
612-
// Day of week: 5
627+
// Year: 2024
628+
// Hour field is not supported for LocalDate
629+
}
630+
631+
// ExampleLocalDate_GetField_advancedFields demonstrates advanced date field queries.
632+
func ExampleLocalDate_GetField_advancedFields() {
633+
date := goda.MustNewLocalDate(2024, goda.March, 15)
634+
635+
// Day of year (1-366)
636+
dayOfYear := date.GetField(goda.DayOfYear)
637+
fmt.Printf("Day of year: %d\n", dayOfYear.Int())
638+
639+
// Epoch days (days since 1970-01-01)
640+
epochDay := date.GetField(goda.EpochDay)
641+
fmt.Printf("Days since Unix epoch: %d\n", epochDay.Int64())
642+
643+
// Proleptic month (months since year 0)
644+
prolepticMonth := date.GetField(goda.ProlepticMonth)
645+
fmt.Printf("Proleptic month: %d\n", prolepticMonth.Int64())
646+
647+
// Era (0=BCE, 1=CE)
648+
era := date.GetField(goda.Era)
649+
if era.Int() == 1 {
650+
fmt.Println("Era: CE (Common Era)")
651+
}
652+
653+
// Output:
613654
// Day of year: 75
655+
// Days since Unix epoch: 19797
656+
// Proleptic month: 24290
657+
// Era: CE (Common Era)
614658
}
615659

616-
// ExampleLocalTime_GetFieldInt64 demonstrates getting field values from a time.
617-
func ExampleLocalTime_GetFieldInt64() {
660+
// ExampleLocalTime_GetField demonstrates querying time fields with TemporalValue.
661+
func ExampleLocalTime_GetField() {
618662
t := goda.MustNewLocalTime(14, 30, 45, 123456789)
619663

620-
fmt.Printf("Hour: %d\n", t.GetFieldInt64(goda.HourOfDay))
621-
fmt.Printf("Minute: %d\n", t.GetFieldInt64(goda.MinuteOfHour))
622-
fmt.Printf("Second: %d\n", t.GetFieldInt64(goda.SecondOfMinute))
623-
fmt.Printf("Millisecond: %d\n", t.GetFieldInt64(goda.MilliOfSecond))
624-
fmt.Printf("AM/PM: %d\n", t.GetFieldInt64(goda.AmPmOfDay))
664+
// Query various time fields
665+
hour := t.GetField(goda.HourOfDay)
666+
if hour.Valid() {
667+
fmt.Printf("Hour of day (0-23): %d\n", hour.Int())
668+
}
669+
670+
minute := t.GetField(goda.MinuteOfHour)
671+
if minute.Valid() {
672+
fmt.Printf("Minute of hour: %d\n", minute.Int())
673+
}
674+
675+
second := t.GetField(goda.SecondOfMinute)
676+
if second.Valid() {
677+
fmt.Printf("Second of minute: %d\n", second.Int())
678+
}
679+
680+
nanos := t.GetField(goda.NanoOfSecond)
681+
if nanos.Valid() {
682+
fmt.Printf("Nanoseconds: %d\n", nanos.Int())
683+
}
684+
685+
// Query unsupported field (date field on time)
686+
dayOfMonth := t.GetField(goda.DayOfMonth)
687+
if dayOfMonth.Unsupported() {
688+
fmt.Println("DayOfMonth field is not supported for LocalTime")
689+
}
625690

626691
// Output:
627-
// Hour: 14
628-
// Minute: 30
629-
// Second: 45
630-
// Millisecond: 123
631-
// AM/PM: 1
692+
// Hour of day (0-23): 14
693+
// Minute of hour: 30
694+
// Second of minute: 45
695+
// Nanoseconds: 123456789
696+
// DayOfMonth field is not supported for LocalTime
697+
}
698+
699+
// ExampleLocalTime_GetField_clockHours demonstrates 12-hour clock field queries.
700+
func ExampleLocalTime_GetField_clockHours() {
701+
// Afternoon time (2:30 PM)
702+
afternoon := goda.MustNewLocalTime(14, 30, 0, 0)
703+
704+
// 24-hour format
705+
hourOfDay := afternoon.GetField(goda.HourOfDay)
706+
fmt.Printf("24-hour format: %d:30\n", hourOfDay.Int())
707+
708+
// 12-hour format components
709+
hourOfAmPm := afternoon.GetField(goda.HourOfAmPm)
710+
amPm := afternoon.GetField(goda.AmPmOfDay)
711+
amPmStr := "AM"
712+
if amPm.Int() == 1 {
713+
amPmStr = "PM"
714+
}
715+
fmt.Printf("12-hour format: %d:30 %s\n", hourOfAmPm.Int(), amPmStr)
716+
717+
// Clock hour (1-12 instead of 0-11)
718+
clockHour := afternoon.GetField(goda.ClockHourOfAmPm)
719+
fmt.Printf("Clock hour (1-12): %d:30 %s\n", clockHour.Int(), amPmStr)
720+
721+
// Midnight special case
722+
midnight := goda.MustNewLocalTime(0, 0, 0, 0)
723+
midnightClock := midnight.GetField(goda.ClockHourOfDay)
724+
fmt.Printf("Midnight clock hour: %d:00\n", midnightClock.Int())
725+
726+
// Output:
727+
// 24-hour format: 14:30
728+
// 12-hour format: 2:30 PM
729+
// Clock hour (1-12): 2:30 PM
730+
// Midnight clock hour: 24:00
731+
}
732+
733+
// ExampleLocalTime_GetField_ofDayFields demonstrates querying cumulative daily values.
734+
func ExampleLocalTime_GetField_ofDayFields() {
735+
t := goda.MustNewLocalTime(14, 30, 45, 500000000) // 2:30:45.5 PM
736+
737+
// Total seconds elapsed since midnight
738+
secondOfDay := t.GetField(goda.SecondOfDay)
739+
fmt.Printf("Seconds since midnight: %d\n", secondOfDay.Int())
740+
741+
// Total minutes elapsed since midnight
742+
minuteOfDay := t.GetField(goda.MinuteOfDay)
743+
fmt.Printf("Minutes since midnight: %d\n", minuteOfDay.Int())
744+
745+
// Total milliseconds elapsed since midnight
746+
milliOfDay := t.GetField(goda.MilliOfDay)
747+
fmt.Printf("Milliseconds since midnight: %d\n", milliOfDay.Int64())
748+
749+
// Total nanoseconds elapsed since midnight
750+
nanoOfDay := t.GetField(goda.NanoOfDay)
751+
fmt.Printf("Nanoseconds since midnight: %d\n", nanoOfDay.Int64())
752+
753+
// Output:
754+
// Seconds since midnight: 52245
755+
// Minutes since midnight: 870
756+
// Milliseconds since midnight: 52245500
757+
// Nanoseconds since midnight: 52245500000000
758+
}
759+
760+
// ExampleLocalDateTime_GetField demonstrates querying fields from a date-time.
761+
func ExampleLocalDateTime_GetField() {
762+
dt := goda.MustNewLocalDateTimeFromComponents(2024, goda.March, 15, 14, 30, 45, 123456789)
763+
764+
// Query date fields
765+
year := dt.GetField(goda.YearField)
766+
month := dt.GetField(goda.MonthOfYear)
767+
day := dt.GetField(goda.DayOfMonth)
768+
769+
if year.Valid() && month.Valid() && day.Valid() {
770+
fmt.Printf("Date: %04d-%02d-%02d\n", year.Int(), month.Int(), day.Int())
771+
}
772+
773+
// Query time fields
774+
hour := dt.GetField(goda.HourOfDay)
775+
minute := dt.GetField(goda.MinuteOfHour)
776+
second := dt.GetField(goda.SecondOfMinute)
777+
778+
if hour.Valid() && minute.Valid() && second.Valid() {
779+
fmt.Printf("Time: %02d:%02d:%02d\n", hour.Int(), minute.Int(), second.Int())
780+
}
781+
782+
// Query day of week
783+
dayOfWeek := dt.GetField(goda.DayOfWeekField)
784+
if dayOfWeek.Valid() {
785+
fmt.Printf("Day of week: %d (Friday)\n", dayOfWeek.Int())
786+
}
787+
788+
// Output:
789+
// Date: 2024-03-15
790+
// Time: 14:30:45
791+
// Day of week: 5 (Friday)
792+
}
793+
794+
// ExampleLocalDateTime_GetField_delegation demonstrates field delegation.
795+
func ExampleLocalDateTime_GetField_delegation() {
796+
dt := goda.MustNewLocalDateTimeFromComponents(2024, goda.March, 15, 14, 30, 45, 0)
797+
798+
// LocalDateTime delegates date fields to LocalDate
799+
dayOfYear := dt.GetField(goda.DayOfYear)
800+
fmt.Printf("Day of year: %d\n", dayOfYear.Int())
801+
802+
// LocalDateTime delegates time fields to LocalTime
803+
nanoOfDay := dt.GetField(goda.NanoOfDay)
804+
fmt.Printf("Nanoseconds of day: %d\n", nanoOfDay.Int64())
805+
806+
// Unsupported fields return unsupported TemporalValue
807+
offsetSeconds := dt.GetField(goda.OffsetSeconds)
808+
if offsetSeconds.Unsupported() {
809+
fmt.Println("OffsetSeconds is not supported for LocalDateTime")
810+
}
811+
812+
// Output:
813+
// Day of year: 75
814+
// Nanoseconds of day: 52245000000000
815+
// OffsetSeconds is not supported for LocalDateTime
632816
}

0 commit comments

Comments
 (0)