File: daterange.go

package info (click to toggle)
golang-github-rickb777-date 1.20.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 460 kB
  • sloc: sh: 63; makefile: 3
file content (308 lines) | stat: -rw-r--r-- 11,830 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
// Copyright 2015 Rick Beton. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package timespan

import (
	"fmt"
	"time"

	"github.com/rickb777/date"
	"github.com/rickb777/date/period"
)

const minusOneNano time.Duration = -1

// DateRange carries a date and a number of days and describes a range between two dates.
type DateRange struct {
	mark date.Date
	days date.PeriodOfDays
}

// NewDateRangeOf assembles a new date range from a start time and a duration, discarding
// the precise time-of-day information. The start time includes a location, which is not
// necessarily UTC. The duration can be negative.
func NewDateRangeOf(start time.Time, duration time.Duration) DateRange {
	sd := date.NewAt(start)
	ed := date.NewAt(start.Add(duration))
	return DateRange{sd, date.PeriodOfDays(ed.Sub(sd))}
}

// NewDateRange assembles a new date range from two dates. These are half-open, so
// if start and end are the same, the range spans zero (not one) day. Similarly, if they
// are on subsequent days, the range is one date (not two).
// The result is normalised.
func NewDateRange(start, end date.Date) DateRange {
	if end.Before(start) {
		return DateRange{end, date.PeriodOfDays(start.Sub(end))}
	}
	return DateRange{start, date.PeriodOfDays(end.Sub(start))}
}

// NewYearOf constructs the range encompassing the whole year specified.
func NewYearOf(year int) DateRange {
	start := date.New(year, time.January, 1)
	end := date.New(year+1, time.January, 1)
	return DateRange{start, date.PeriodOfDays(end.Sub(start))}
}

// NewMonthOf constructs the range encompassing the whole month specified for a given year.
// It handles leap years correctly.
func NewMonthOf(year int, month time.Month) DateRange {
	start := date.New(year, month, 1)
	endT := time.Date(year, month+1, 1, 0, 0, 0, 0, time.UTC)
	end := date.NewAt(endT)
	return DateRange{start, date.PeriodOfDays(end.Sub(start))}
}

// EmptyRange constructs an empty range. This is often a useful basis for
// further operations but note that the end date is undefined.
func EmptyRange(day date.Date) DateRange {
	return DateRange{day, 0}
}

// OneDayRange constructs a range of exactly one day. This is often a useful basis for
// further operations. Note that the last date is the same as the start date.
func OneDayRange(day date.Date) DateRange {
	return DateRange{day, 1}
}

// DayRange constructs a range of n days.
//
// Note that n can be negative. In this case, the specified day will be the end day,
// which is outside of the half-open range; the last day will be the day before the
// day specified.
func DayRange(day date.Date, n date.PeriodOfDays) DateRange {
	if n < 0 {
		return DateRange{day.Add(n), -n}
	}
	return DateRange{day, n}
}

// Days returns the period represented by this range. This will never be negative.
func (dateRange DateRange) Days() date.PeriodOfDays {
	if dateRange.days < 0 {
		return -dateRange.days
	}
	return dateRange.days
}

// IsZero returns true if this has a zero start date and the the range is empty.
// Usually this is because the range was created via the zero value.
func (dateRange DateRange) IsZero() bool {
	return dateRange.days == 0 && dateRange.mark.IsZero()
}

// IsEmpty returns true if this has a starting date but the range is empty (zero days).
func (dateRange DateRange) IsEmpty() bool {
	return dateRange.days == 0
}

// Start returns the earliest date represented by this range.
func (dateRange DateRange) Start() date.Date {
	if dateRange.days < 0 {
		return dateRange.mark.Add(date.PeriodOfDays(1 + dateRange.days))
	}
	return dateRange.mark
}

// Last returns the last date (inclusive) represented by this range. Be careful because
// if the range is empty (i.e. has zero days), then the last is undefined so an empty date
// is returned. Therefore it is often more useful to use End() instead of Last().
// See also IsEmpty().
func (dateRange DateRange) Last() date.Date {
	if dateRange.days < 0 {
		return dateRange.mark // because mark is at the end
	} else if dateRange.days == 0 {
		return date.Date{}
	}
	return dateRange.mark.Add(dateRange.days - 1)
}

// End returns the date following the last date of the range. End can be considered to
// be the exclusive end, i.e. the final value of a half-open range.
//
// If the range is empty (i.e. has zero days), then the start date is returned, this being
// also the (half-open) end value in that case. This is more useful than the undefined result
// returned by Last() for empty ranges.
func (dateRange DateRange) End() date.Date {
	if dateRange.days < 0 {
		return dateRange.mark.Add(1) // because mark is at the end
	}
	return dateRange.mark.Add(dateRange.days)
}

// Normalise ensures that the number of days is zero or positive.
// The normalised date range is returned;
// in this value, the mark date is the same as the start date.
func (dateRange DateRange) Normalise() DateRange {
	if dateRange.days < 0 {
		return DateRange{dateRange.mark.Add(dateRange.days), -dateRange.days}
	}
	return dateRange
}

// ShiftBy moves the date range by moving both the start and end dates similarly.
// A negative parameter is allowed.
func (dateRange DateRange) ShiftBy(days date.PeriodOfDays) DateRange {
	if days == 0 {
		return dateRange
	}
	newMark := dateRange.mark.Add(days)
	return DateRange{newMark, dateRange.days}
}

// ExtendBy extends (or reduces) the date range by moving the end date.
// A negative parameter is allowed and this may cause the range to become inverted
// (i.e. the mark date becomes the end date instead of the start date).
func (dateRange DateRange) ExtendBy(days date.PeriodOfDays) DateRange {
	if days == 0 {
		return dateRange
	}
	return DateRange{dateRange.mark, dateRange.days + days}.Normalise()
}

// ShiftByPeriod moves the date range by moving both the start and end dates similarly.
// A negative parameter is allowed.
//
// Any time component is ignored. Therefore, be careful with periods containing
// more that 24 hours in the hours/minutes/seconds fields. These will not be
// normalised for you; if you want this behaviour, call delta.Normalise(false)
// on the input parameter.
//
// For example, PT24H adds nothing, whereas P1D adds one day as expected. To
// convert a period such as PT24H to its equivalent P1D, use
// delta.Normalise(false) as the input.
func (dateRange DateRange) ShiftByPeriod(delta period.Period) DateRange {
	if delta.IsZero() {
		return dateRange
	}
	newMark := dateRange.mark.AddPeriod(delta)
	//fmt.Printf("mark + %v : %v -> %v", delta, dateRange.mark, newMark)
	return DateRange{newMark, dateRange.days}
}

// ExtendByPeriod extends (or reduces) the date range by moving the end date.
// A negative parameter is allowed and this may cause the range to become inverted
// (i.e. the mark date becomes the end date instead of the start date).
func (dateRange DateRange) ExtendByPeriod(delta period.Period) DateRange {
	if delta.IsZero() {
		return dateRange
	}
	newEnd := dateRange.End().AddPeriod(delta)
	//fmt.Printf("%v, end + %v : %v -> %v", dateRange.mark, delta, dateRange.End(), newEnd)
	return NewDateRange(dateRange.Start(), newEnd)
}

// String describes the date range in human-readable form.
func (dateRange DateRange) String() string {
	norm := dateRange.Normalise()
	switch norm.days {
	case 0:
		return fmt.Sprintf("0 days at %s", norm.mark)
	case 1:
		return fmt.Sprintf("1 day on %s", norm.mark)
	default:
		return fmt.Sprintf("%d days from %s to %s", norm.days, norm.Start(), norm.Last())
	}
}

// Contains tests whether the date range contains a specified date.
// Empty date ranges (i.e. zero days) never contain anything.
func (dateRange DateRange) Contains(d date.Date) bool {
	if dateRange.days == 0 {
		return false
	}
	return !(d.Before(dateRange.Start()) || d.After(dateRange.Last()))
}

// StartUTC assumes that the start date is a UTC date and gets the start time of that date, as UTC.
// It returns midnight on the first day of the range.
func (dateRange DateRange) StartUTC() time.Time {
	return dateRange.Start().UTC()
}

// EndUTC assumes that the end date is a UTC date and returns the time a nanosecond after the end time
// in a specified location. Along with StartUTC, this gives a 'half-open' range where the start
// is inclusive and the end is exclusive.
func (dateRange DateRange) EndUTC() time.Time {
	return dateRange.End().UTC()
}

// ContainsTime tests whether a given local time is within the date range. The time range is
// from midnight on the start day to one nanosecond before midnight on the day after the end date.
// Empty date ranges (i.e. zero days) never contain anything.
//
// If a calculation needs to be 'half-open' (i.e. the end date is exclusive), simply use the
// expression 'dateRange.ExtendBy(-1).ContainsTime(t)'
func (dateRange DateRange) ContainsTime(t time.Time) bool {
	if dateRange.days == 0 {
		return false
	}
	utc := t.In(time.UTC)
	return !(utc.Before(dateRange.StartUTC()) || dateRange.EndUTC().Add(minusOneNano).Before(utc))
}

// Merge combines two date ranges by calculating a date range that just encompasses them both.
// There are two special cases.
//
// Firstly, if one range is entirely contained within the other range, the larger of the two is
// returned. Otherwise, the result is from the start of the earlier one to the end of the later
// one, even if the two ranges don't overlap.
//
// Secondly, if either range is the zero value (see IsZero), it is excluded from the merge and
// the other range is returned unchanged.
func (dateRange DateRange) Merge(otherRange DateRange) DateRange {
	if otherRange.IsZero() {
		return dateRange
	}
	if dateRange.IsZero() {
		return otherRange
	}
	minStart := dateRange.Start().Min(otherRange.Start())
	maxEnd := dateRange.End().Max(otherRange.End())
	return NewDateRange(minStart, maxEnd)
}

// Duration computes the duration (in nanoseconds) from midnight at the start of the date
// range up to and including the very last nanosecond before midnight on the end day.
// The calculation is for UTC, which does not have daylight saving and every day has 24 hours.
//
// If the range is greater than approximately 290 years, the result will hard-limit to the
// minimum or maximum possible duration (see time.Sub(t)).
func (dateRange DateRange) Duration() time.Duration {
	return dateRange.End().UTC().Sub(dateRange.Start().UTC())
}

// DurationIn computes the duration (in nanoseconds) from midnight at the start of the date
// range up to and including the very last nanosecond before midnight on the end day.
// The calculation is for the specified location, which may have daylight saving, so not every day
// necessarily has 24 hours. If the date range spans the day the clocks are changed, this is
// taken into account.
//
// If the range is greater than approximately 290 years, the result will hard-limit to the
// minimum or maximum possible duration (see time.Sub(t)).
func (dateRange DateRange) DurationIn(loc *time.Location) time.Duration {
	return dateRange.EndTimeIn(loc).Sub(dateRange.StartTimeIn(loc))
}

// StartTimeIn returns the start time in a specified location.
func (dateRange DateRange) StartTimeIn(loc *time.Location) time.Time {
	return dateRange.Start().In(loc)
}

// EndTimeIn returns the nanosecond after the end time in a specified location. Along with
// StartTimeIn, this gives a 'half-open' range where the start is inclusive and the end is
// exclusive.
func (dateRange DateRange) EndTimeIn(loc *time.Location) time.Time {
	return dateRange.End().In(loc)
}

// TimeSpanIn obtains the time span corresponding to the date range in a specified location.
// The result is normalised.
func (dateRange DateRange) TimeSpanIn(loc *time.Location) TimeSpan {
	s := dateRange.StartTimeIn(loc)
	d := dateRange.DurationIn(loc)
	return TimeSpan{s, d}
}