diff --git a/go.mod b/go.mod index de2a871..e9c2bb3 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/uuid v1.5.0 // indirect diff --git a/go.sum b/go.sum index 03730c3..4ff6264 100644 --- a/go.sum +++ b/go.sum @@ -86,6 +86,8 @@ github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0 github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= diff --git a/goextVersion.go b/goextVersion.go index 91cf331..43eb628 100644 --- a/goextVersion.go +++ b/goextVersion.go @@ -1,5 +1,5 @@ package goext -const GoextVersion = "0.0.584" +const GoextVersion = "0.0.585" -const GoextVersionTimestamp = "2025-06-26T16:48:07+0200" +const GoextVersionTimestamp = "2025-07-04T11:46:00+0200" diff --git a/timeext/time.go b/timeext/time.go index fb4837c..3ba5240 100644 --- a/timeext/time.go +++ b/timeext/time.go @@ -3,6 +3,7 @@ package timeext import ( "fmt" "math" + "sort" "time" ) @@ -142,6 +143,38 @@ func Max(a time.Time, b time.Time) time.Time { } } +func Avg(v ...time.Time) time.Time { + if len(v) == 0 { + return time.Time{} + } + + var sum int64 + for _, t := range v { + sum += t.UnixNano() + } + + return time.Unix(0, sum/int64(len(v))) +} + +func Median(v ...time.Time) time.Time { + if len(v) == 0 { + return time.Time{} + } + + sorted := make([]time.Time, len(v)) + copy(sorted, v) + sort.Slice(sorted, func(i, j int) bool { + return sorted[i].UnixNano() < sorted[j].UnixNano() + }) + + mid := len(sorted) / 2 + if len(sorted)%2 == 0 { + return Avg(sorted[mid-1], sorted[mid]) + } else { + return sorted[mid] + } +} + func UnixFloatSeconds(v float64) time.Time { sec, dec := math.Modf(v) return time.Unix(int64(sec), int64(dec*(1e9))) diff --git a/timeext/time_test.go b/timeext/time_test.go index 24c980c..13dfc4d 100644 --- a/timeext/time_test.go +++ b/timeext/time_test.go @@ -227,3 +227,173 @@ func TestDaysInMonth_FebruaryNonLeapYear(t *testing.T) { t.Errorf("Expected %d but got %d", expected, result) } } + +func TestAvg_MultipleValues(t *testing.T) { + t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) + t2 := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC) + t3 := time.Date(2022, 1, 5, 0, 0, 0, 0, time.UTC) + + // Average should be January 3, 2022 (middle date) + expected := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC) + result := Avg(t1, t2, t3) + + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestAvg_TwoValues(t *testing.T) { + t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) + t2 := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC) + + expected := time.Date(2022, 1, 2, 0, 0, 0, 0, time.UTC) + result := Avg(t1, t2) + + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestAvg_EmptySlice(t *testing.T) { + result := Avg() + expected := time.Time{} + + if !result.Equal(expected) { + t.Errorf("Expected zero time but got %v", result) + } +} + +func TestMedian_OddNumberOfValues(t *testing.T) { + t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) + t2 := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC) + t3 := time.Date(2022, 1, 5, 0, 0, 0, 0, time.UTC) + + // Median should be the middle date + expected := t2 + result := Median(t1, t2, t3) + + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestMedian_EvenNumberOfValues(t *testing.T) { + t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) + t2 := time.Date(2022, 1, 2, 0, 0, 0, 0, time.UTC) + t3 := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC) + t4 := time.Date(2022, 1, 4, 0, 0, 0, 0, time.UTC) + + // Median for even number of values should be average of middle two + expected := time.Date(2022, 1, 2, 12, 0, 0, 0, time.UTC) + result := Median(t1, t2, t3, t4) + + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestMedian_UnsortedValues(t *testing.T) { + t1 := time.Date(2022, 1, 5, 0, 0, 0, 0, time.UTC) + t2 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) + t3 := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC) + + // Median should correctly sort values first + expected := t3 + result := Median(t1, t2, t3) + + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestMedian_EmptySlice(t *testing.T) { + result := Median() + expected := time.Time{} + + if !result.Equal(expected) { + t.Errorf("Expected zero time but got %v", result) + } +} + +func TestTimeToDatePart(t *testing.T) { + tz := TimezoneBerlin + tm := time.Date(2022, 1, 1, 13, 14, 15, 0, tz) + expected := time.Date(2022, 1, 1, 0, 0, 0, 0, tz) + result := TimeToDatePart(tm, tz) + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestTimeToWeekStart(t *testing.T) { + tz := TimezoneBerlin + // January 5, 2022 was a Wednesday + tm := time.Date(2022, 1, 5, 13, 14, 15, 0, tz) + // Should return Monday, January 3, 2022 + expected := time.Date(2022, 1, 3, 0, 0, 0, 0, tz) + result := TimeToWeekStart(tm, tz) + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestTimeToWeekStart_WhenMonday(t *testing.T) { + tz := TimezoneBerlin + // January 3, 2022 was a Monday + tm := time.Date(2022, 1, 3, 13, 14, 15, 0, tz) + expected := time.Date(2022, 1, 3, 0, 0, 0, 0, tz) + result := TimeToWeekStart(tm, tz) + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestTimeToMonthStart(t *testing.T) { + tz := TimezoneBerlin + tm := time.Date(2022, 1, 15, 13, 14, 15, 0, tz) + expected := time.Date(2022, 1, 1, 0, 0, 0, 0, tz) + result := TimeToMonthStart(tm, tz) + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestTimeToMonthEnd(t *testing.T) { + tz := TimezoneBerlin + tm := time.Date(2022, 1, 15, 13, 14, 15, 0, tz) + expected := time.Date(2022, 2, 1, 0, 0, 0, 0, tz).Add(-1) + result := TimeToMonthEnd(tm, tz) + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestTimeToYearStart(t *testing.T) { + tz := TimezoneBerlin + tm := time.Date(2022, 5, 15, 13, 14, 15, 0, tz) + expected := time.Date(2022, 1, 1, 0, 0, 0, 0, tz) + result := TimeToYearStart(tm, tz) + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestTimeToYearEnd(t *testing.T) { + tz := TimezoneBerlin + tm := time.Date(2022, 5, 15, 13, 14, 15, 0, tz) + expected := time.Date(2023, 1, 1, 0, 0, 0, 0, tz).Add(-1) + result := TimeToYearEnd(tm, tz) + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +} + +func TestTimeToNextYearStart(t *testing.T) { + tz := TimezoneBerlin + tm := time.Date(2022, 5, 15, 13, 14, 15, 0, tz) + expected := time.Date(2023, 1, 1, 0, 0, 0, 0, tz) + result := TimeToNextYearStart(tm, tz) + if !result.Equal(expected) { + t.Errorf("Expected %v but got %v", expected, result) + } +}