| package datetime |
|
|
| import ( |
| "testing" |
| "time" |
|
|
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
|
|
| "github.com/openmeterio/openmeter/pkg/clock" |
| ) |
|
|
| func TestDurationDivisibleBy(t *testing.T) { |
| tests := []struct { |
| name string |
| larger string |
| smaller string |
| expected bool |
| hasError bool |
| }{ |
| |
| { |
| name: "1 year divisible by 1 year", |
| larger: "P1Y", |
| smaller: "P1Y", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "2 years divisible by 1 year", |
| larger: "P2Y", |
| smaller: "P1Y", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "2 years divisible by 2 months", |
| larger: "P2Y", |
| smaller: "P2M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 1 month", |
| larger: "P1Y", |
| smaller: "P1M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 1 day", |
| larger: "P1Y", |
| smaller: "P1D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 1 hour", |
| larger: "P1Y", |
| smaller: "PT1H", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 2 months", |
| larger: "P1Y", |
| smaller: "P2M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 3 months", |
| larger: "P1Y", |
| smaller: "P3M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 4 months", |
| larger: "P1Y", |
| smaller: "P4M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 6 months", |
| larger: "P1Y", |
| smaller: "P6M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 month divisible by 1 day", |
| larger: "P1M", |
| smaller: "P1D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "6 months divisible by 2 months", |
| larger: "P6M", |
| smaller: "P2M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "6 months divisible by 3 months", |
| larger: "P6M", |
| smaller: "P3M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "4 weeks divisible by 1 week", |
| larger: "P4W", |
| smaller: "P1W", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "Same periods should be divisible", |
| larger: "P1M", |
| smaller: "P1M", |
| expected: true, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "1 month not divisible by 3 days", |
| larger: "P1M", |
| smaller: "P3D", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 month not divisible by 1 week", |
| larger: "P1M", |
| smaller: "P1W", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 year not divisible by 5 months", |
| larger: "P1Y", |
| smaller: "P5M", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 year not divisible by 7 months", |
| larger: "P1Y", |
| smaller: "P7M", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 year not divisible by 5 days", |
| larger: "P1Y", |
| smaller: "P5D", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 year not divisible by 365 days", |
| larger: "P1Y", |
| smaller: "P365D", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 year not divisible by 8 hours", |
| larger: "P1Y", |
| smaller: "PT8H", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 month not divisible by 5 hour", |
| larger: "P1M", |
| smaller: "PT5H", |
| expected: false, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "anything not divisible by zero period", |
| larger: "P1D", |
| smaller: "PT0S", |
| expected: false, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "smaller period larger than larger period", |
| larger: "P1D", |
| smaller: "P1M", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "smaller period larger than larger period 2", |
| larger: "P1W", |
| smaller: "P1M", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "smaller period larger than larger period 3", |
| larger: "P4W", |
| smaller: "P1M", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 month smaller than 1 year", |
| larger: "P1M", |
| smaller: "P1Y", |
| expected: false, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "30 days divisible by 5 days", |
| larger: "P30D", |
| smaller: "P5D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "30 days divisible by 6 days", |
| larger: "P30D", |
| smaller: "P6D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "30 days not divisible by 7 days", |
| larger: "P30D", |
| smaller: "P7D", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "28 days divisible by 7 days", |
| larger: "P28D", |
| smaller: "P7D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "365 days divisible by 1 day", |
| larger: "P365D", |
| smaller: "P1D", |
| expected: true, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "8 weeks divisible by 2 weeks", |
| larger: "P8W", |
| smaller: "P2W", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "8 weeks not divisible by 3 weeks", |
| larger: "P8W", |
| smaller: "P3W", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "52 weeks divisible by 4 weeks", |
| larger: "P52W", |
| smaller: "P4W", |
| expected: true, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "2 years divisible by 6 months", |
| larger: "P2Y", |
| smaller: "P6M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "18 months divisible by 3 months", |
| larger: "P18M", |
| smaller: "P3M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "18 months divisible by 6 months", |
| larger: "P18M", |
| smaller: "P6M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "18 months not divisible by 5 months", |
| larger: "P18M", |
| smaller: "P5M", |
| expected: false, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "1 week divisible by 1 day", |
| larger: "P1W", |
| smaller: "P1D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "2 weeks divisible by 7 days", |
| larger: "P2W", |
| smaller: "P7D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "14 days divisible by 1 week", |
| larger: "P14D", |
| smaller: "P1W", |
| expected: true, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "period with hours only should work", |
| larger: "PT24H", |
| smaller: "PT12H", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "48 hours divisible by 24 hours", |
| larger: "PT48H", |
| smaller: "PT24H", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "72 hours divisible by 24 hours", |
| larger: "PT72H", |
| smaller: "PT24H", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "25 hours not divisible by 24 hours", |
| larger: "PT25H", |
| smaller: "PT24H", |
| expected: false, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "10 minutes divisible by 5 minutes", |
| larger: "PT10M", |
| smaller: "PT5M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "60 seconds divisible by 30 seconds", |
| larger: "PT60S", |
| smaller: "PT30S", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 hour 30 minutes divisible by 30 minutes", |
| larger: "PT1H30M", |
| smaller: "PT30M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "2 hours divisible by 1 hour 30 seconds (not exact)", |
| larger: "PT2H", |
| smaller: "PT1H30S", |
| expected: false, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "very large periods - 10 years divisible by 1 year", |
| larger: "P10Y", |
| smaller: "P1Y", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "very large periods - 100 years divisible by 10 years", |
| larger: "P100Y", |
| smaller: "P10Y", |
| expected: true, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "2 hours divisible by 1 hour", |
| larger: "PT2H", |
| smaller: "PT1H", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "3 days divisible by 1 day", |
| larger: "P3D", |
| smaller: "P1D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "5 hours not divisible by 3 hours", |
| larger: "PT5H", |
| smaller: "PT3H", |
| expected: false, |
| hasError: false, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| larger, err := ISODurationString(tt.larger).Parse() |
| assert.NoError(t, err) |
|
|
| smaller, err := ISODurationString(tt.smaller).Parse() |
| assert.NoError(t, err) |
|
|
| result, err := larger.DivisibleBy(smaller) |
|
|
| if tt.hasError { |
| assert.Error(t, err) |
| } else { |
| assert.NoError(t, err) |
| assert.Equal(t, tt.expected, result, "Expected %s to be divisible by %s: %v", tt.larger, tt.smaller, tt.expected) |
| } |
| }) |
| } |
| } |
|
|
| func TestISODurationOperations(t *testing.T) { |
| t.Run("Parse", func(t *testing.T) { |
| isoDuration := "P1Y2M3DT4H5M6S" |
|
|
| period, err := ISODurationString(isoDuration).Parse() |
| if err != nil { |
| t.Fatalf("unexpected error: %v", err) |
| } |
|
|
| now := MustParseDateTime(t, "2020-01-01T00:00:00Z").Time |
|
|
| expected := MustParseDateTime(t, "2021-03-04T04:05:06Z").Time |
| actual, precise := period.AddTo(now) |
| assert.True(t, precise) |
| assert.Equal(t, expected, actual) |
| }) |
|
|
| t.Run("ParseError", func(t *testing.T) { |
| isoDuration := "P1Y2M3DT4H5M6SX" |
|
|
| _, err := ISODurationString(isoDuration).Parse() |
| assert.NotNil(t, err) |
| }) |
|
|
| t.Run("Works with 0 duration", func(t *testing.T) { |
| isoDuration := "PT0S" |
|
|
| period, err := ISODurationString(isoDuration).Parse() |
| if err != nil { |
| t.Fatalf("unexpected error: %v", err) |
| } |
|
|
| now := MustParseDateTime(t, "2020-01-01T00:00:00Z").Time |
|
|
| expected := MustParseDateTime(t, "2020-01-01T00:00:00Z").Time |
| actual, precise := period.AddTo(now) |
| assert.True(t, precise) |
| assert.Equal(t, expected, actual) |
| }) |
|
|
| t.Run("Adding periods", func(t *testing.T) { |
| isoDuration1 := "PT5M" |
| isoDuration2 := "PT1M1S" |
|
|
| period1, err := ISODurationString(isoDuration1).Parse() |
| if err != nil { |
| t.Fatalf("unexpected error: %v", err) |
| } |
|
|
| period2, err := ISODurationString(isoDuration2).Parse() |
| if err != nil { |
| t.Fatalf("unexpected error: %v", err) |
| } |
|
|
| expectedS := "PT6M1S" |
| expected, err := ISODurationString(expectedS).Parse() |
| if err != nil { |
| t.Fatalf("unexpected error: %v", err) |
| } |
|
|
| actual, err := period1.Add(period2) |
| if err != nil { |
| t.Fatalf("unexpected error: %v", err) |
| } |
|
|
| assert.Equal(t, expected, actual) |
| }) |
| } |
|
|
| func TestDivisibleBy(t *testing.T) { |
| tests := []struct { |
| name string |
| larger string |
| smaller string |
| expected bool |
| hasError bool |
| }{ |
| |
| { |
| name: "1 year divisible by 1 year", |
| larger: "P1Y", |
| smaller: "P1Y", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "2 years divisible by 1 year", |
| larger: "P2Y", |
| smaller: "P1Y", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "2 years divisible by 2 months", |
| larger: "P2Y", |
| smaller: "P2M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 1 month", |
| larger: "P1Y", |
| smaller: "P1M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 1 day", |
| larger: "P1Y", |
| smaller: "P1D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 1 hour", |
| larger: "P1Y", |
| smaller: "PT1H", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 8 hours", |
| larger: "P1Y", |
| smaller: "PT8H", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 2 months", |
| larger: "P1Y", |
| smaller: "P2M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 3 months", |
| larger: "P1Y", |
| smaller: "P3M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 4 months", |
| larger: "P1Y", |
| smaller: "P4M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 year divisible by 6 months", |
| larger: "P1Y", |
| smaller: "P6M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "1 month divisible by 1 day", |
| larger: "P1M", |
| smaller: "P1D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "6 months divisible by 2 months", |
| larger: "P6M", |
| smaller: "P2M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "6 months divisible by 3 months", |
| larger: "P6M", |
| smaller: "P3M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "4 weeks divisible by 1 week", |
| larger: "P4W", |
| smaller: "P1W", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "Same periods should be divisible", |
| larger: "P1M", |
| smaller: "P1M", |
| expected: true, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "1 month not divisible by 3 days", |
| larger: "P1M", |
| smaller: "P3D", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 month not divisible by 1 week", |
| larger: "P1M", |
| smaller: "P1W", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 year not divisible by 5 months", |
| larger: "P1Y", |
| smaller: "P5M", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 year not divisible by 7 months", |
| larger: "P1Y", |
| smaller: "P7M", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 year not divisible by 5 days", |
| larger: "P1Y", |
| smaller: "P5D", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 year not divisible by 365 days", |
| larger: "P1Y", |
| smaller: "P365D", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 month not divisible by 5 hour", |
| larger: "P1M", |
| smaller: "PT5H", |
| expected: false, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "zero period not divisible by anything", |
| larger: "PT0S", |
| smaller: "P1D", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "anything not divisible by zero period", |
| larger: "P1D", |
| smaller: "PT0S", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "zero period not divisible by zero period", |
| larger: "PT0S", |
| smaller: "PT0S", |
| expected: false, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "smaller period larger than larger period", |
| larger: "P1D", |
| smaller: "P1M", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "smaller period larger than larger period 2", |
| larger: "P1W", |
| smaller: "P1M", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "smaller period larger than larger period 2", |
| larger: "P4W", |
| smaller: "P1M", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "1 month smaller than 1 year", |
| larger: "P1M", |
| smaller: "P1Y", |
| expected: false, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "30 days divisible by 5 days", |
| larger: "P30D", |
| smaller: "P5D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "30 days divisible by 6 days", |
| larger: "P30D", |
| smaller: "P6D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "30 days not divisible by 7 days", |
| larger: "P30D", |
| smaller: "P7D", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "28 days divisible by 7 days", |
| larger: "P28D", |
| smaller: "P7D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "365 days divisible by 1 day", |
| larger: "P365D", |
| smaller: "P1D", |
| expected: true, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "8 weeks divisible by 2 weeks", |
| larger: "P8W", |
| smaller: "P2W", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "8 weeks not divisible by 3 weeks", |
| larger: "P8W", |
| smaller: "P3W", |
| expected: false, |
| hasError: false, |
| }, |
| { |
| name: "52 weeks divisible by 4 weeks", |
| larger: "P52W", |
| smaller: "P4W", |
| expected: true, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "2 years divisible by 6 months", |
| larger: "P2Y", |
| smaller: "P6M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "18 months divisible by 3 months", |
| larger: "P18M", |
| smaller: "P3M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "18 months divisible by 6 months", |
| larger: "P18M", |
| smaller: "P6M", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "18 months not divisible by 5 months", |
| larger: "P18M", |
| smaller: "P5M", |
| expected: false, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "1 week divisible by 1 day", |
| larger: "P1W", |
| smaller: "P1D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "2 weeks divisible by 7 days", |
| larger: "P2W", |
| smaller: "P7D", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "14 days divisible by 1 week", |
| larger: "P14D", |
| smaller: "P1W", |
| expected: true, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "period with hours only should work", |
| larger: "PT24H", |
| smaller: "PT12H", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "48 hours divisible by 24 hours", |
| larger: "PT48H", |
| smaller: "PT24H", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "72 hours divisible by 24 hours", |
| larger: "PT72H", |
| smaller: "PT24H", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "25 hours not divisible by 24 hours", |
| larger: "PT25H", |
| smaller: "PT24H", |
| expected: false, |
| hasError: false, |
| }, |
|
|
| |
| { |
| name: "very large periods - 10 years divisible by 1 year", |
| larger: "P10Y", |
| smaller: "P1Y", |
| expected: true, |
| hasError: false, |
| }, |
| { |
| name: "very large periods - 100 years divisible by 10 years", |
| larger: "P100Y", |
| smaller: "P10Y", |
| expected: true, |
| hasError: false, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| larger, err := ISODurationString(tt.larger).Parse() |
| assert.NoError(t, err) |
|
|
| smaller, err := ISODurationString(tt.smaller).Parse() |
| assert.NoError(t, err) |
|
|
| result, err := larger.DivisibleBy(smaller) |
|
|
| if tt.hasError { |
| assert.Error(t, err) |
| } else { |
| assert.NoError(t, err) |
| assert.Equal(t, tt.expected, result, "Expected %s to be divisible by %s: %v", tt.larger, tt.smaller, tt.expected) |
| } |
| }) |
| } |
| } |
|
|
| func TestAddTo(t *testing.T) { |
| |
|
|
| t.Run("should preserve monotonic component 1", func(t *testing.T) { |
| oneMin := NewISODuration(0, 0, 0, 0, 0, 1, 0) |
|
|
| clock.FreezeTime(time.Now()) |
| defer clock.UnFreeze() |
|
|
| now := clock.Now() |
|
|
| res, ok := oneMin.AddTo(now) |
| assert.True(t, ok) |
|
|
| assert.Equal(t, now.Add(time.Minute), res) |
| }) |
|
|
| t.Run("should preserve monotonic component 2", func(t *testing.T) { |
| oneMin := NewISODuration(0, 0, 0, 0, 0, 1, 0) |
|
|
| now := clock.Now() |
|
|
| res, ok := oneMin.AddTo(now) |
| assert.True(t, ok) |
|
|
| assert.Equal(t, now.Add(time.Minute), res) |
| }) |
|
|
| t.Run("should preserve monotonic component 3", func(t *testing.T) { |
| oneMin := NewISODuration(0, 0, 0, 0, 0, 1, 0) |
|
|
| now := time.Now() |
|
|
| res, ok := oneMin.AddTo(now) |
| assert.True(t, ok) |
|
|
| assert.Equal(t, now.Add(time.Minute), res) |
| }) |
|
|
| t.Run("should preserve non-existence of monotonic component 1", func(t *testing.T) { |
| oneMin := NewISODuration(0, 0, 0, 0, 0, 1, 0) |
|
|
| now, err := time.Parse(time.RFC3339Nano, "2025-01-01T00:00:00.000000001Z") |
| require.NoError(t, err) |
|
|
| res, ok := oneMin.AddTo(now) |
| assert.True(t, ok) |
|
|
| assert.Equal(t, now.Add(time.Minute), res) |
| }) |
|
|
| t.Run("should preserve non-existence of monotonic component 2", func(t *testing.T) { |
| oneMin := NewISODuration(0, 0, 0, 0, 0, 1, 0) |
|
|
| now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) |
|
|
| res, ok := oneMin.AddTo(now) |
| assert.True(t, ok) |
|
|
| assert.Equal(t, now.Add(time.Minute), res) |
| }) |
| } |
|
|