-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add JamTime, TimeSlot and Epoch implementation
- Loading branch information
Showing
9 changed files
with
415 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,12 @@ | ||
module github.com/eigerco/strawberry | ||
|
||
go 1.22.5 | ||
|
||
require github.com/go-quicktest/qt v1.101.0 | ||
|
||
require ( | ||
github.com/google/go-cmp v0.5.9 // indirect | ||
github.com/kr/pretty v0.3.1 // indirect | ||
github.com/kr/text v0.2.0 // indirect | ||
github.com/rogpeppe/go-internal v1.11.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= | ||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= | ||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | ||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= | ||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | ||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= | ||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= | ||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package jamtime | ||
|
||
import ( | ||
"errors" | ||
"time" | ||
) | ||
|
||
const ( | ||
TimeslotsPerEpoch = 600 | ||
EpochDuration = TimeslotsPerEpoch * TimeslotDuration // 1 hour | ||
) | ||
|
||
// Epoch represents a JAM Epoch | ||
type Epoch uint32 | ||
|
||
// ToEpoch converts a JamTime to its corresponding Epoch | ||
func (jt JamTime) ToEpoch() Epoch { | ||
return Epoch(jt.Seconds / uint64(EpochDuration.Seconds())) | ||
} | ||
|
||
// FromEpoch creates a JamTime from an Epoch (start of the epoch) | ||
func FromEpoch(e Epoch) JamTime { | ||
return JamTime{Seconds: uint64(e) * uint64(EpochDuration.Seconds())} | ||
} | ||
|
||
// ToEpoch converts a Timeslot to its corresponding Epoch | ||
func (ts Timeslot) ToEpoch() Epoch { | ||
return Epoch(ts / TimeslotsPerEpoch) | ||
} | ||
|
||
// CurrentEpoch returns the current epoch | ||
func CurrentEpoch() Epoch { | ||
return Now().ToEpoch() | ||
} | ||
|
||
// EpochStart returns the JamTime at the start of the epoch | ||
func (e Epoch) EpochStart() JamTime { | ||
return FromEpoch(e) | ||
} | ||
|
||
// EpochEnd returns the JamTime at the end of the epoch | ||
func (e Epoch) EpochEnd() JamTime { | ||
return FromEpoch(e + 1).Add(-time.Nanosecond) | ||
} | ||
|
||
// NextEpoch returns the next epoch | ||
func (e Epoch) NextEpoch() Epoch { | ||
return e + 1 | ||
} | ||
|
||
// PreviousEpoch returns the previous epoch | ||
func (e Epoch) PreviousEpoch() Epoch { | ||
return e - 1 | ||
} | ||
|
||
// ValidateEpoch checks if a given Epoch is within the valid range | ||
func ValidateEpoch(e Epoch) error { | ||
jamTime := FromEpoch(e) | ||
return ValidateJamTime(jamTime.ToTime()) | ||
} | ||
|
||
// EpochAndTimeslotToJamTime converts an Epoch and a timeslot within that epoch to JamTime | ||
func EpochAndTimeslotToJamTime(e Epoch, timeslotInEpoch uint32) (JamTime, error) { | ||
if timeslotInEpoch >= TimeslotsPerEpoch { | ||
return JamTime{}, errors.New("timeslot number exceeds epoch length") | ||
} | ||
epochStart := FromEpoch(e) | ||
return JamTime{Seconds: epochStart.Seconds + uint64(timeslotInEpoch)*uint64(TimeslotDuration.Seconds())}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package jamtime | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"time" | ||
) | ||
|
||
var now = time.Now | ||
|
||
// JamEpoch represents the start of the JAM Common Era | ||
var JamEpoch = time.Date(2024, time.January, 1, 12, 0, 0, 0, time.UTC) | ||
|
||
// JamTime represents a time in the JAM Common Era | ||
type JamTime struct { | ||
Seconds uint64 | ||
} | ||
|
||
// Now returns the current time as a JamTime | ||
func Now() JamTime { | ||
return FromTime(now()) | ||
} | ||
|
||
// FromTime converts a standard time.Time to JamTime | ||
func FromTime(t time.Time) JamTime { | ||
duration := t.Sub(JamEpoch) | ||
seconds := uint64(duration.Seconds()) | ||
return JamTime{Seconds: seconds} | ||
} | ||
|
||
// ToTime converts a JamTime to a standard time.Time | ||
func (jt JamTime) ToTime() time.Time { | ||
duration := time.Duration(jt.Seconds) * time.Second | ||
return JamEpoch.Add(duration) | ||
} | ||
|
||
// FromSeconds creates a JamTime from the number of seconds since the JAM Epoch | ||
func FromSeconds(seconds uint64) JamTime { | ||
return JamTime{Seconds: seconds} | ||
} | ||
|
||
// Before reports whether the time instant jt is before u | ||
func (jt JamTime) Before(u JamTime) bool { | ||
return jt.Seconds < u.Seconds | ||
} | ||
|
||
// After reports whether the time instant jt is after u | ||
func (jt JamTime) After(u JamTime) bool { | ||
return jt.Seconds > u.Seconds | ||
} | ||
|
||
// Equal reports whether jt and u represent the same time instant | ||
func (jt JamTime) Equal(u JamTime) bool { | ||
return jt.Seconds == u.Seconds | ||
} | ||
|
||
// Add returns the time jt+d | ||
func (jt JamTime) Add(d time.Duration) JamTime { | ||
return JamTime{Seconds: jt.Seconds + uint64(d.Seconds())} | ||
} | ||
|
||
// Sub returns the duration jt-u | ||
func (jt JamTime) Sub(u JamTime) time.Duration { | ||
return time.Duration(int64(jt.Seconds-u.Seconds)) * time.Second | ||
} | ||
|
||
// IsInFutureTimeSlot checks if a given JamTime is in a future timeslot | ||
func (jt JamTime) IsInFutureTimeSlot() bool { | ||
return jt.ToTimeslot() > CurrentTimeslot() | ||
} | ||
|
||
// ToTimeslot converts a JamTime to its corresponding Timeslot | ||
func (jt JamTime) ToTimeslot() Timeslot { | ||
return Timeslot(jt.Seconds / uint64(TimeslotDuration.Seconds())) | ||
} | ||
|
||
// MarshalJSON implements the json.Marshaler interface | ||
func (jt JamTime) MarshalJSON() ([]byte, error) { | ||
return []byte(fmt.Sprintf(`"%s"`, []byte(jt.ToTime().Format(time.RFC3339)))), nil | ||
} | ||
|
||
// UnmarshalJSON implements the json.Unmarshaler interface | ||
func (jt *JamTime) UnmarshalJSON(data []byte) error { | ||
t, err := time.Parse(`"`+time.RFC3339+`"`, string(data)) | ||
if err != nil { | ||
return err | ||
} | ||
*jt = FromTime(t) | ||
return nil | ||
} | ||
|
||
// JamTimeToEpochAndTimeslot converts a JamTime to its Epoch and timeslot within that epoch | ||
func (jt JamTime) ToEpochAndTimeslot() (Epoch, uint32) { | ||
epoch := jt.ToEpoch() | ||
timeslotInEpoch := uint32((jt.Seconds / uint64(TimeslotDuration.Seconds())) % TimeslotsPerEpoch) | ||
return epoch, timeslotInEpoch | ||
} | ||
|
||
// IsInSameEpoch checks if two JamTimes are in the same epoch | ||
func (jt JamTime) IsInSameEpoch(other JamTime) bool { | ||
return jt.ToEpoch() == other.ToEpoch() | ||
} | ||
|
||
// ValidateJamTime checks if a given time.Time is within the valid range for JamTime | ||
func ValidateJamTime(t time.Time) error { | ||
if t.Before(JamEpoch) { | ||
return errors.New("time is before JAM Epoch") | ||
} | ||
if t.After(time.Date(2840, time.August, 15, 23, 59, 59, 999999999, time.UTC)) { | ||
return errors.New("time is after maximum representable JAM time") | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
package jamtime | ||
|
||
import ( | ||
"encoding/json" | ||
"testing" | ||
"time" | ||
|
||
"github.com/go-quicktest/qt" | ||
) | ||
|
||
func TestJamTimeConversion(t *testing.T) { | ||
t.Run("successfully convert to to and from JamTime", func(t *testing.T) { | ||
standardTime := time.Date(2025, time.March, 15, 12, 0, 0, 0, time.UTC) | ||
jamTime := FromTime(standardTime) | ||
convertedTime := jamTime.ToTime() | ||
|
||
qt.Assert(t, qt.IsTrue(standardTime.Equal(convertedTime))) | ||
|
||
secondsInYear := uint64(31_536_000) | ||
jamTime = FromSeconds(secondsInYear) | ||
expectedTime := JamEpoch.Add(time.Duration(secondsInYear) * time.Second) | ||
|
||
qt.Assert(t, qt.IsTrue(jamTime.ToTime().Equal(expectedTime))) | ||
}) | ||
} | ||
|
||
func TestJamTimeComparison(t *testing.T) { | ||
t.Run("non equal", func(t *testing.T) { | ||
t1 := FromSeconds(1000) | ||
t2 := FromSeconds(2000) | ||
|
||
qt.Assert(t, qt.IsTrue(t1.Before(t2))) | ||
qt.Assert(t, qt.IsTrue(t2.After(t1))) | ||
qt.Assert(t, qt.IsFalse(t1.Equal(t2))) | ||
}) | ||
|
||
t.Run("equal", func(t *testing.T) { | ||
t1 := FromSeconds(1000) | ||
t2 := FromSeconds(1000) | ||
|
||
qt.Assert(t, qt.IsTrue(t1.Equal(t2))) | ||
qt.Assert(t, qt.IsTrue(t2.Equal(t1))) | ||
}) | ||
} | ||
|
||
func TestJamTimeArithmetic(t *testing.T) { | ||
t.Run("adding jamtime", func(t *testing.T) { | ||
t1 := FromSeconds(1000) | ||
duration := 500 * time.Second | ||
|
||
t2 := t1.Add(duration) | ||
qt.Assert(t, qt.Not(qt.Equals(t2, t1))) | ||
qt.Assert(t, qt.Equals(time.Duration(t2.Seconds)*time.Second, time.Duration(1500)*time.Second)) | ||
}) | ||
|
||
t.Run("subbing jamtime", func(t *testing.T) { | ||
t1 := FromSeconds(1000) | ||
t2 := FromSeconds(500) | ||
|
||
duration := t1.Sub(t2) | ||
qt.Assert(t, qt.Equals(duration, time.Duration(500)*time.Second)) | ||
}) | ||
} | ||
|
||
func TestJamTimeJSON(t *testing.T) { | ||
t.Run("successfully marshal to json", func(t *testing.T) { | ||
jamTime := FromSeconds(1000) | ||
jsonData, err := json.Marshal(jamTime) | ||
if err != nil { | ||
t.Fatalf("JSON marshaling failed: %v", err) | ||
} | ||
|
||
qt.Assert(t, qt.DeepEquals(jsonData, []uint8(`"2024-01-01T12:16:40Z"`))) | ||
}) | ||
|
||
t.Run("successfully unmarshal jamtime", func(t *testing.T) { | ||
jsonData := []byte(`"2024-01-01T12:00:00Z"`) | ||
|
||
var unmarshaledTime JamTime | ||
err := json.Unmarshal(jsonData, &unmarshaledTime) | ||
if err != nil { | ||
t.Fatalf("JSON unmarshaling failed: %v", err) | ||
} | ||
|
||
qt.Assert(t, qt.IsTrue(unmarshaledTime.ToTime().Equal(JamEpoch))) | ||
}) | ||
|
||
t.Run("successfully unmarshal jamtime in future", func(t *testing.T) { | ||
jsonData := []byte(`"2024-01-01T12:00:01Z"`) | ||
|
||
var unmarshaledTime JamTime | ||
err := json.Unmarshal(jsonData, &unmarshaledTime) | ||
if err != nil { | ||
t.Fatalf("JSON unmarshaling failed: %v", err) | ||
} | ||
|
||
want := JamEpoch.Add(1 * time.Second) | ||
|
||
qt.Assert(t, qt.IsTrue(unmarshaledTime.ToTime().Equal(want))) | ||
}) | ||
} | ||
|
||
func TestJamTimeFromToTimeslotConversion(t *testing.T) { | ||
t.Run("convert jamtime to timeslot", func(t *testing.T) { | ||
jamTime := FromSeconds(3600) // 10 minutes after JAM Epoch | ||
timeslot := jamTime.ToTimeslot() | ||
|
||
qt.Assert(t, qt.Equals(uint32(timeslot), 600)) | ||
}) | ||
|
||
t.Run("convert timeslot to jamtime", func(t *testing.T) { | ||
slot := Timeslot(100) | ||
|
||
jamTime := FromTimeslot(slot) | ||
|
||
qt.Assert(t, qt.Equals(jamTime.Seconds, 600)) | ||
}) | ||
} | ||
|
||
func TestJamTimeIsInFutureTimeSlot(t *testing.T) { | ||
currentTime := Now() | ||
pastTime := currentTime.Add(-5 * time.Minute) | ||
futureTime := currentTime.Add(10 * time.Minute) | ||
|
||
qt.Assert(t, qt.IsFalse(currentTime.IsInFutureTimeSlot())) | ||
qt.Assert(t, qt.IsFalse(pastTime.IsInFutureTimeSlot())) | ||
qt.Assert(t, qt.IsTrue(futureTime.IsInFutureTimeSlot())) | ||
} | ||
|
||
func TestEpochConversion(t *testing.T) { | ||
t.Run("jamtime to epoch", func(t *testing.T) { | ||
jamTime := FromSeconds(3600) // 1 hour after JAM Epoch | ||
epoch := jamTime.ToEpoch() | ||
|
||
qt.Assert(t, qt.Equals(epoch, 1)) | ||
}) | ||
|
||
t.Run("epoch to jamtime", func(t *testing.T) { | ||
e := Epoch(1) | ||
|
||
convertedJamTime := FromEpoch(e) | ||
|
||
qt.Assert(t, qt.Equals(convertedJamTime.Seconds, 3600)) | ||
}) | ||
} |
Oops, something went wrong.