Skip to content

Commit

Permalink
feat: add JamTime, TimeSlot and Epoch implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
aranw committed Jul 25, 2024
1 parent c775ed3 commit af4032f
Show file tree
Hide file tree
Showing 9 changed files with 415 additions and 15 deletions.
9 changes: 9 additions & 0 deletions go.mod
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
)
13 changes: 13 additions & 0 deletions go.sum
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=
69 changes: 69 additions & 0 deletions internal/jamtime/epoch.go
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
}
113 changes: 113 additions & 0 deletions internal/jamtime/jamtime.go
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
}
145 changes: 145 additions & 0 deletions internal/jamtime/jamtime_test.go
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))
})
}
Loading

0 comments on commit af4032f

Please sign in to comment.