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.

This section is empty.

View Source

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.

func MustNewDecimal(years, months, weeks, days, hours, minutes, seconds decimal.Decimal) Period

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.

func MustParse[S ISOString | string](isoPeriod S) Period

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.

func New(years, months, weeks, days, hours, minutes, seconds int) Period

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.

func NewHMS(hours, minutes, seconds int) Period

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.

func NewYMD(years, months, days int) Period

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)

func NewYMWD(years, months, weeks, days int) Period

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".

func (period Period) Abs() Period

Abs converts a negative period to a positive period.

func (period Period) Add(other Period) (Period, error)

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>.

func (period Period) Days() int

Days gets the whole number of days in the period.

DaysDecimal gets the number of days in the period, including any fraction present.

func (period Period) DaysIncWeeks() int

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.

func (period *Period) Get() any

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.

func (period Period) Hours() int

Hours gets the whole number of hours in the period.

HoursDecimal gets the number of hours in the period, including any fraction present.

func (period Period) IsNegative() bool

IsNegative returns true if the period is negative.

func (period Period) IsPositive() bool

IsPositive returns true if the period is positive or zero.

func (period Period) IsZero() bool

IsZero returns true if applied to a period of zero length.

MarshalText implements the encoding.TextMarshaler interface for Periods. This also provides support for JSON encoding.

func (period Period) Minutes() int

Minutes gets the whole number of minutes in the period.

MinutesDecimal gets the number of minutes in the period, including any fraction present.

func (period Period) Months() int

Months gets the whole number of months in the period.

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.

func (period Period) Negate() Period

Negate changes the sign of the period. Zero is not altered.

func (period Period) Normalise(precise bool) Period

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.

func (period Period) NormaliseDaysToYears() Period

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()
func (period Period) OnlyHMS() Period

OnlyHMS returns the period with only the hour, minute and second fields. The year, month, week and day fields are zeroed.

func (period Period) OnlyYMWD() Period

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".

func (period Period) Period() ISOString

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).

func (period Period) Seconds() int

Seconds gets the whole number of seconds in the period.

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.

func (period Period) Sign() int

Sign returns 1 if the period is positive, -1 if it is negative, or zero otherwise.

func (period Period) Simplify(precise bool) Period

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.

func (period Period) SimplifyWeeks() Period

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.

func (period Period) SimplifyWeeksToDays() Period

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).

func (period Period) Subtract(other Period) (Period, error)

Subtract subtracts one period from another. Arithmetic overflow will result in an error.

func (period Period) TotalDaysApprox() int

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.

func (period Period) TotalMonthsApprox() int

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.

func (period *Period) UnmarshalText(data []byte) error

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,

func (period Period) Weeks() int

Weeks gets the whole number of weeks in the period.

WeeksDecimal gets the number of weeks in the period, including any fraction present.

WriteTo converts the period to ISO-8601 form.

func (period Period) Years() int

Years gets the whole number of years in the period.

YearsDecimal gets the number of years in the period, including any fraction present.