period package - github.com/rickb777/period - Go Packages
Package period provides functionality for periods of time using ISO-8601 conventions. This deals with years, months, weeks, days, hours, minutes and seconds.
Because of the vagaries of calendar systems, the meaning of year lengths, month lengths and even day lengths depends on context. So a period is not necessarily a fixed duration of time in terms of seconds. The type time.Duration is measured in terms of nanoseconds. Periods can be converted to/from durations: depending on the length of period, this may be calculated exactly or approximately.
The two main types here are
- period.ISOString is a string holding an ISO-8601 period
- period.Period containing seven numbers: years, months, weeks, days, hours, minutes and seconds
These can be converted to the other.
The period defined in this API is specified by ISO-8601, but that uses the term 'duration' instead; see https://en.wikipedia.org/wiki/ISO_8601#Durations. In Go, time.Duration and this period.Period and period.ISOString follow terminology similar to e.g. Joda Time (https://www.joda.org/joda-time/key_period.html):
- a 'duration' is a definite number of seconds (or fractions of a second),
- a 'period' refers to human chronology of years, months, weeks, days, hours, minutes and seconds
The iCalendar standard (RFC-5545) also defines durations based on the ISO-8601 definitions, see https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.6.
Example period.ISOString representations:
- "P2Y" is two years;
- "P6M" is six months;
- "P1W" is one week (seven days);
- "P4D" is four days;
- "PT3H" is three hours.
- "PT20M" is twenty minutes.
- "PT30S" is thirty seconds.
- "-PT30S" or "PT-30S" is minus thirty seconds (implies an "earlier" time).
These can be combined, for example:
- "P3Y11M4W1D" is 3 years, 11 months, 4 weeks and 1 day, which is nearly 4 years.
- "P2DT12H" is 2 days and 12 hours.
- "P1M-1D" is 1 month minus 1 day. Mixed signs are permitted but may not be widely supported elsewhere.
Also, decimal fractions are supported. To comply with the standard, only the last non-zero component is allowed to have a fraction. For example
- "P2.5Y" or "P2,5Y" is 2.5 years; both notations are allowed.
- "PT12M7.497S" is 12 minutes and 7.497 seconds.
- Variables
- type Designator
- type FormatLocalisation
- type ISOString
- type Period
- func Between(t1, t2 time.Time) Period
- func MustNewDecimal(years, months, weeks, days, hours, minutes, seconds decimal.Decimal) Period
- func MustParse[S ISOString | string](isoPeriod S) Period
- func New(years, months, weeks, days, hours, minutes, seconds int) Period
- func NewDecimal(years, months, weeks, days, hours, minutes, seconds decimal.Decimal) (period Period, err error)
- func NewHMS(hours, minutes, seconds int) Period
- func NewOf(duration time.Duration) Period
- func NewYMD(years, months, days int) Period
- func NewYMWD(years, months, weeks, days int) Period
- func Parse[S ISOString | string](isoPeriod S) (Period, error)
- func (period Period) Abs() Period
- func (period Period) Add(other Period) (Period, error)
- func (period Period) AddTo(t time.Time) (time.Time, bool)
- func (period Period) Days() int
- func (period Period) DaysDecimal() decimal.Decimal
- func (period Period) DaysIncWeeks() int
- func (period Period) DaysIncWeeksDecimal() decimal.Decimal
- func (period Period) Duration() (time.Duration, bool)
- func (period Period) DurationApprox() time.Duration
- func (period Period) Format() string
- func (period Period) FormatLocalised(config FormatLocalisation) string
- func (period *Period) Get() any
- func (period Period) GetField(field Designator) decimal.Decimal
- func (period Period) GetInt(field Designator) int
- func (period Period) Hours() int
- func (period Period) HoursDecimal() decimal.Decimal
- func (period Period) IsNegative() bool
- func (period Period) IsPositive() bool
- func (period Period) IsZero() bool
- func (period Period) MarshalBinary() ([]byte, error)
- func (period Period) MarshalText() ([]byte, error)
- func (period Period) Minutes() int
- func (period Period) MinutesDecimal() decimal.Decimal
- func (period Period) Months() int
- func (period Period) MonthsDecimal() decimal.Decimal
- func (period Period) Mul(factor decimal.Decimal) (Period, error)
- func (period Period) Negate() Period
- func (period Period) Normalise(precise bool) Period
- func (period Period) NormaliseDaysToYears() Period
- func (period Period) OnlyHMS() Period
- func (period Period) OnlyYMWD() Period
- func (period *Period) Parse(isoPeriod string) error
- func (period Period) Period() ISOString
- func (period *Period) Scan(value interface{}) (err error)
- func (period Period) Seconds() int
- func (period Period) SecondsDecimal() decimal.Decimal
- func (period *Period) Set(p string) error
- func (period Period) SetField(value decimal.Decimal, field Designator) (Period, error)
- func (period Period) SetInt(value int, field Designator) Period
- func (period Period) Sign() int
- func (period Period) Simplify(precise bool) Period
- func (period Period) SimplifyWeeks() Period
- func (period Period) SimplifyWeeksToDays() Period
- func (period Period) String() string
- func (period Period) Subtract(other Period) (Period, error)
- func (period Period) TotalDaysApprox() int
- func (period Period) TotalMonthsApprox() int
- func (period Period) Type() string
- func (period *Period) UnmarshalBinary(data []byte) error
- func (period *Period) UnmarshalText(data []byte) error
- func (period Period) Value() (driver.Value, error)
- func (period Period) Weeks() int
- func (period Period) WeeksDecimal() decimal.Decimal
- func (period Period) WriteTo(w io.Writer) (int64, error)
- func (period Period) Years() int
- func (period Period) YearsDecimal() decimal.Decimal
This section is empty.
var DefaultFormatLocalisation = FormatLocalisation{ ZeroValue: "zero", Negate: func(s string) string { return "minus " + s }, YearNames: plural.FromZero("", "%v year", "%v years"), MonthNames: plural.FromZero("", "%v month", "%v months"), WeekNames: plural.FromZero("", "%v week", "%v weeks"), DayNames: plural.FromZero("", "%v day", "%v days"), HourNames: plural.FromZero("", "%v hour", "%v hours"), MinuteNames: plural.FromZero("", "%v minute", "%v minutes"), SecondNames: plural.FromZero("", "%v second", "%v seconds"), }
DefaultFormatLocalisation provides the formatting strings needed to format Period values in vernacular English.
Zero is the zero period.
This section is empty.
Designator enumerates the seven fields in a Period.
const ( Second Designator Minute Hour Day Week Month Year )
ISOString holds a period of time and provides conversion to/from ISO-8601 representations. Therefore, there are seven fields: years, months, weeks, days, hours, minutes, and seconds.
In the ISO representation, decimal fractions are supported, although only the last non-zero component is allowed to have a fraction according to the Standard. For example "P2.5Y" is 2.5 years.
const CanonicalZero ISOString = "P0D"
CanonicalZero is the zero length period in one of its possible representations.
ISOString returns the string.
Period holds a period of time as a set of decimal numbers, one for each field in the ISO-8601 period.
By conventional, all the fields should have the same sign. However, this is not restricted, so each field after the first non-zero field can be independently positive or negative. Sometimes this makes sense, e.g. "P1YT-1S" is one second less than one year.
The precision is large: all fields are scaled decimals using int64 internally for calculations. The value of each field can have up to 19 digits (the range of int64), of which up to 19 digits can be a decimal fraction. So the range is much wider than that of time.Duration; be aware that periods more than 292 years or less than one nanosecond are outside the convertible range.
For convenience, the method inputs and outputs use int.
Fractions are supported on the least significant non-zero field only. It is an error for more-significant fields to have fractional values too.
Instances are immutable.
Between converts the span between two times to a period. Based on the Gregorian conversion algorithms of `time.Time`, the resultant period is precise.
If t2 is before t1, the result is a negative period.
The result just a number of seconds, possibly including a fraction. It is not normalised; see Period.Normalise.
Remember that the resultant period does not retain any knowledge of the calendar, so any subsequent computations applied to the period can only be precise if they concern either the date (year, month, day) part, or the clock (hour, minute, second) part, but not both.
MustNewDecimal creates a period from seven decimal values. The fields are trimmed but no normalisation is applied, e.g. 120 seconds will not become 2 minutes. Use Period.Normalise if you need to.
Periods only allow the least-significant non-zero field to contain a fraction. If any of the more-significant fields is supplied with a fraction, this function panics.
MustParse is as per period.Parse except that it panics if the string cannot be parsed. This is intended for setup code; don't use it for user inputs.
New creates a simple period without any fractional parts. The fields are initialised verbatim without any normalisation; e.g. 120 seconds will not become 2 minutes. Use Period.Normalise if you need to.
func NewDecimal(years, months, weeks, days, hours, minutes, seconds decimal.Decimal) (period Period, err error)
NewDecimal creates a period from seven decimal values. The fields are trimmed but no normalisation is applied, e.g. 120 seconds will not become 2 minutes. Use Period.Normalise if you need to.
Periods only allow the least-significant non-zero field to contain a fraction. If any of the more-significant fields is supplied with a fraction, an error will be returned. This can be safely ignored for non-standard behaviour.
NewHMS creates a simple period without any fractional parts. The fields are initialised verbatim without any normalisation; e.g. 120 seconds will not become 2 minutes. Use Period.Normalise if you need to.
NewOf converts a time duration to a Period. The result just a number of seconds, possibly including a fraction. It is not normalised; see Period.Normalise.
NewYMD creates a simple period without any fractional parts. The fields are initialised verbatim without any normalisation; e.g. 12 months will not become 1 year. Use Period.Normalise if you need to.
This function is equivalent to NewYMWD(years, months, 0, days)
NewYMWD creates a simple period without any fractional parts. The fields are initialised verbatim without any normalisation; e.g. 12 months will not become 1 year. Use Period.Normalise if you need to.
Parse parses strings that specify periods using ISO-8601 rules.
In addition, a plus or minus sign can precede the period, e.g. "-P10D"
It is possible to mix a number of weeks with other fields (e.g. P2M1W), although this would not be allowed by ISO-8601. See Period.SimplifyWeeks.
The zero value can be represented in several ways: all of the following are equivalent: "P0Y", "P0M", "P0W", "P0D", "PT0H", PT0M", PT0S", and "P0". The canonical zero is "P0D".
Add adds two periods together. Use this method along with Period.Negate in order to subtract periods. Arithmetic overflow will result in an error.
AddTo adds the period to a time, returning the result. A flag is also returned that is true when the conversion was precise, and false otherwise.
When the period specifies hours, minutes and seconds only, the result is precise.
Similarly, when the period specifies whole years, months, weeks and days (i.e. without fractions), the result is precise.
However, when years, months or days contains fractions, the result is only an approximation (it assumes that all days are 24 hours and every year is 365.2425 days, as per Gregorian calendar rules).
Note: the use of AddDate has unintended consequences when considering the addition of a time only period. See <https://x.com/joelmcourtney/status/1803301619955904979>.
DaysDecimal gets the number of days in the period, including any fraction present.
DaysIncWeeks gets the whole number of days in the period, including all the weeks. The result is d + (w * 7), given d days and w weeks.
See also Period.SimplifyWeeksToDays.
DaysIncWeeksDecimal gets the number of days in the period, including all the weeks and including any fraction present. The result is d + (w * 7), given d days and w weeks.
See also Period.SimplifyWeeksToDays.
Duration converts a period to the equivalent duration in nanoseconds. A flag is also returned that is true when the conversion was precise, and false otherwise.
When the period specifies hours, minutes and seconds only, the result is precise. However, when the period specifies years, months, weeks and days, it is impossible to be precise because the result may depend on knowing date and timezone information. So the duration is estimated on the basis of a year being 365.2425 days (as per Gregorian calendar rules) and a month being 1/12 of a that; days are all assumed to be 24 hours long.
For periods shorter than one nanosecond, the duration will be zero and the precise flag will be returned false.
Note that time.Duration is limited to the range 1 nanosecond to about 292 years maximum.
DurationApprox converts a period to the equivalent duration in nanoseconds. When the period specifies hours, minutes and seconds only, the result is precise. however, when the period specifies years, months, weeks and days, it is impossible to be precise because the result may depend on knowing date and timezone information. So the duration is estimated on the basis of a year being 365.2425 days (as per Gregorian calendar rules) and a month being 1/12 of a that; days are all assumed to be 24 hours long.
Note that time.Duration is limited to the range 1 nanosecond to about 292 years maximum.
Format converts the period to human-readable form using DefaultFormatLocalisation. To adjust the result, see the Period.Normalise, Period.NormaliseDaysToYears, Period.Simplify and Period.SimplifyWeeksToDays methods.
func (period Period) FormatLocalised(config FormatLocalisation) string
FormatLocalised converts the period to human-readable form in a localisable way. To adjust the result, see the Period.Normalise, Period.NormaliseDaysToYears, Period.Simplify and Period.SimplifyWeeksToDays methods.
Get enables use of Period by the flag API. It is therefore possible to use Period values in flag parameters.
GetField gets one field.
A panic arises if the field is unknown.
func (period Period) GetInt(field Designator) int
GetInt gets one field as a whole number.
A panic arises if the field is unknown.
HoursDecimal gets the number of hours in the period, including any fraction present.
MarshalText implements the encoding.TextMarshaler interface for Periods. This also provides support for JSON encoding.
MinutesDecimal gets the number of minutes in the period, including any fraction present.
MonthsDecimal gets the number of months in the period, including any fraction present.
Mul multiplies a period by a factor. Obviously, this can both enlarge and shrink it, and change the sign if the factor is negative. The result is not normalised.
Normalise simplifies the fields by propagating large values towards the more significant fields.
Because the number of hours per day is imprecise (due to daylight savings etc), and because the number of days per month is variable in the Gregorian calendar, there is a reluctance to transfer time to or from the days element. To give control over this, there are two modes: it operates in either precise or approximate mode.
- Multiples of 60 seconds become minutes - both modes.
- Multiples of 60 minutes become hours - both modes.
- Multiples of 24 hours become days - approximate mode only
- Multiples of 7 days become weeks - both modes.
- Multiples of 12 months become years - both modes.
Note that leap seconds are disregarded: every minute is assumed to have 60 seconds.
If the calculations would lead to arithmetic errors, the current values are kept unaltered.
See also Period.NormaliseDaysToYears.
NormaliseDaysToYears tries to propagate large numbers of days (and corresponding weeks) to the years field. Based on the Gregorian rule, there are assumed to be 365.2425 days per year.
- Multiples of 365.2425 days become years
If the calculations would lead to arithmetic errors, the current values are kept unaltered.
A common use pattern would be to chain this after Period.Normalise, i.e.
p.Normalise(false).NormaliseDaysToYears()
OnlyHMS returns the period with only the hour, minute and second fields. The year, month, week and day fields are zeroed.
OnlyYMWD returns the period with only the year, month, week and day fields. The hour, minute and second fields are zeroed.
Parse parses strings that specify periods using ISO-8601 rules.
In addition, a plus or minus sign can precede the period, e.g. "-P10D"
It is possible to mix a number of weeks with other fields (e.g. P2M1W), although this would not be allowed by ISO-8601. See Period.SimplifyWeeks.
The zero value can be represented in several ways: all of the following are equivalent: "P0Y", "P0M", "P0W", "P0D", "PT0H", PT0M", PT0S", and "P0". The canonical zero is "P0D".
Period converts the period to ISO-8601 string form. If there is a decimal fraction, it will be rendered using a decimal point separator (not a comma).
SecondsDecimal gets the number of seconds in the period, including any fraction present.
Set enables use of Period by the flag API. It is therefore possible to use Period values in flag parameters.
SetField sets one field in the period. Like NewDecimal, an error arises if the new period would have multiple fields with fractions.
A panic arises if the field is unknown.
func (period Period) SetInt(value int, field Designator) Period
SetInt sets one field in the period as a whole number.
A panic arises if the field is unknown.
Sign returns 1 if the period is positive, -1 if it is negative, or zero otherwise.
Simplify simplifies the fields by propagating large values towards the less significant fields. This is akin to converting mixed fractions to improper fractions, across the group of fields. However, existing fields are not altered if they are a simple way of expressing their period already.
For example, "P2Y1M" simplifies to "P25M" but "P2Y" remains "P2Y".
Because the number of hours per day is imprecise (due to daylight savings etc), and because the number of days per month is variable in the Gregorian calendar, there is a reluctance to transfer time to or from the days element. To give control over this, there are two modes: it operates in either precise or approximate mode.
- Years may become multiples of 12 months if the number of months is non-zero - both modes.
- Weeks - see Period.SimplifyWeeks - both modes.
- Days may become multiples of 24 hours if the number of hours is non-zero - approximate mode only
- Hours may become multiples of 60 minutes if the number of minutes is non-zero - both modes.
- Minutes may become multiples of 60 seconds if the number of seconds is non-zero - both modes.
If the calculations would lead to arithmetic errors, the current values are kept unaltered.
SimplifyWeeks adds 7 * the weeks field to the days field, and sets the weeks field to zero, but only if some other fields are non-zero.
This will increase compatibility with external systems that do not expect to receive a weeks component unless the other components are zero. This is because ISO-8601 periods contain either weeks or other fields but not both.
See also Period.SimplifyWeeksToDays.
SimplifyWeeksToDays adds 7 * the weeks field to the days field, and sets the weeks field to zero. See also Period.SimplifyWeeks.
ISOString converts the period to ISO-8601 string form. If there is a decimal fraction, it will be rendered using a decimal point separator. (not a comma).
Subtract subtracts one period from another. Arithmetic overflow will result in an error.
TotalDaysApprox gets the approximate total number of days in the period. The approximation assumes a year is 365.2425 days as per Gregorian calendar rules) and a month is 1/12 of that. Whole multiples of 24 hours are also included in the calculation.
TotalMonthsApprox gets the approximate total number of months in the period. The days component is included by approximation, assuming a year is 365.2425 days (as per Gregorian calendar rules) and a month is 1/12 of that. Whole multiples of 24 hours are also included in the calculation.
Type is for compatibility with the spf13/pflag library.
UnmarshalText implements the encoding.TextUnmarshaler interface for Periods. This also provides support for JSON decoding.
Value converts the period to an ISO-8601 string. It implements driver.Valuer,
WeeksDecimal gets the number of weeks in the period, including any fraction present.
WriteTo converts the period to ISO-8601 form.
YearsDecimal gets the number of years in the period, including any fraction present.