fix(boundary): preserve instants at minute boundaries
same bug as #377, one level up: StartOfMinute/EndOfMinute rebuilt the time with time.Date, so in a repeated hour new york 01:34:49 EST got a minute ending before it. they now copy the instance and subtract the local seconds, keeping the instant, location and offset (not Truncate(time.Minute), which breaks lmt offsets with seconds).
This commit is contained in:
1 parent
3dbfef56fe
commit
ddbdba233d
2 files changed
+41
-4
No files matched your search
+8
-4
@@ -153,8 +153,10 @@ func (c *Carbon) StartOfMinute() *Carbon {
|
|||||||
if c.IsInvalid() {
|
if c.IsInvalid() {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
year, month, day, hour, minute, _ := c.DateTime()
|
elapsed := time.Duration(c.Second())*time.Second + time.Duration(c.Nanosecond())
|
||||||
return c.create(year, month, day, hour, minute, MinSecond, MinNanosecond)
|
result := c.Copy()
|
||||||
|
result.time = result.time.Add(-elapsed)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndOfMinute returns a Carbon instance for end of the minute.
|
// EndOfMinute returns a Carbon instance for end of the minute.
|
||||||
@@ -162,8 +164,10 @@ func (c *Carbon) EndOfMinute() *Carbon {
|
|||||||
if c.IsInvalid() {
|
if c.IsInvalid() {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
year, month, day, hour, minute, _ := c.DateTime()
|
elapsed := time.Duration(c.Second())*time.Second + time.Duration(c.Nanosecond())
|
||||||
return c.create(year, month, day, hour, minute, MaxSecond, MaxNanosecond)
|
result := c.Copy()
|
||||||
|
result.time = result.time.Add(time.Minute - time.Nanosecond - elapsed)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartOfSecond returns a Carbon instance for start of the second.
|
// StartOfSecond returns a Carbon instance for start of the second.
|
||||||
|
|||||||
@@ -48,6 +48,39 @@ func (s *BoundarySuite) TestCarbon_SecondBoundariesAtOffsetTransitions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *BoundarySuite) TestCarbon_MinuteBoundariesAtOffsetTransitions() {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
zone string
|
||||||
|
inputUTC time.Time
|
||||||
|
}{
|
||||||
|
{"New York first occurrence", "America/New_York", time.Date(2026, 11, 1, 5, 34, 49, 123456789, time.UTC)},
|
||||||
|
{"New York second occurrence", "America/New_York", time.Date(2026, 11, 1, 6, 34, 49, 123456789, time.UTC)},
|
||||||
|
{"Lord Howe first occurrence", "Australia/Lord_Howe", time.Date(2026, 4, 4, 14, 45, 49, 123456789, time.UTC)},
|
||||||
|
{"Lord Howe second occurrence", "Australia/Lord_Howe", time.Date(2026, 4, 4, 15, 15, 49, 123456789, time.UTC)},
|
||||||
|
{"Monrovia second-based offset", "Africa/Monrovia", time.Date(1960, 6, 15, 12, 34, 49, 123456789, time.UTC)},
|
||||||
|
{"UTC control", "UTC", time.Date(2026, 1, 2, 12, 34, 49, 123456789, time.UTC)},
|
||||||
|
} {
|
||||||
|
s.Run(test.name, func() {
|
||||||
|
loc, err := time.LoadLocation(test.zone)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
input := test.inputUTC.In(loc)
|
||||||
|
startUnix := input.Unix() - int64(input.Second())
|
||||||
|
c := NewCarbon(input).SetWeekStartsAt(Sunday)
|
||||||
|
start, end := c.StartOfMinute(), c.EndOfMinute()
|
||||||
|
s.Equal(startUnix, start.StdTime().Unix())
|
||||||
|
s.Equal(startUnix+MaxSecond, end.StdTime().Unix())
|
||||||
|
s.Equal(0, start.Nanosecond())
|
||||||
|
s.Equal(MaxNanosecond, end.Nanosecond())
|
||||||
|
s.Same(loc, start.StdTime().Location())
|
||||||
|
s.Same(loc, end.StdTime().Location())
|
||||||
|
s.Equal(Sunday, start.WeekStartsAt())
|
||||||
|
s.Equal(Sunday, end.WeekStartsAt())
|
||||||
|
s.Equal(input, c.StdTime())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *BoundarySuite) TestCarbon_StartOfCentury() {
|
func (s *BoundarySuite) TestCarbon_StartOfCentury() {
|
||||||
s.Run("nil carbon", func() {
|
s.Run("nil carbon", func() {
|
||||||
var c *Carbon
|
var c *Carbon
|
||||||
|
|||||||
Reference in new issue
Block a user