Skip to content

Commit

Permalink
Period: some improved documentation and a tweak of month calculations
Browse files Browse the repository at this point in the history
  • Loading branch information
rickb777 committed Apr 4, 2020
1 parent e1afd88 commit 2248ec4
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 18 deletions.
37 changes: 22 additions & 15 deletions period/period.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

const daysPerYearE4 int64 = 3652425 // 365.2425 days by the Gregorian rule
const daysPerMonthE4 int64 = 304375 // 30.4375 days per month
const daysPerMonthE4 int64 = 304369 // 30.4369 days per month
const daysPerMonthE6 int64 = 30436875 // 30.436875 days per month

const oneE4 int64 = 10000
Expand Down Expand Up @@ -86,7 +86,13 @@ func New(years, months, days, hours, minutes, seconds int) Period {

// NewOf converts a time duration to a Period, and also indicates whether the conversion is precise.
// Any time duration that spans more than ± 3276 hours will be approximated by assuming that there
// are 24 hours per day, 30.4375 per month and 365.2425 days per year.
// are 24 hours per day, 365.2425 days per year (as per Gregorian calendar rules), and a month
// being 1/12 of that (approximately 30.4369 days).
//
// The result is not always fully normalised; for time differences less than 3276 hours (about 4.5 months),
// it will contain zero in the years, months and days fields but the number of days may be up to 3275; this
// reduces errors arising from the variable lengths of months. For larger time differences, greater than
// 3276 hours, the days, months and years fields are used as well.
func NewOf(duration time.Duration) (p Period, precise bool) {
var sign int16 = 1
d := duration
Expand Down Expand Up @@ -127,10 +133,10 @@ func NewOf(duration time.Duration) (p Period, precise bool) {
// Between converts the span between two times to a period. Based on the Gregorian conversion
// algorithms of `time.Time`, the resultant period is precise.
//
// The result is not normalised; for time differences less than 3276 days, it will contain zero in the
// years and months fields but the number of days may be up to 3275; this reduces errors arising from
// the variable lengths of months. For larger time differences, greater than 3276 days, the months and
// years fields are used as well.
// To improve precision, result is not always fully normalised; for time differences less than 3276 hours
// (about 4.5 months), it will contain zero in the years, months and days fields but the number of hours
// may be up to 3275; this reduces errors arising from the variable lengths of months. For larger time
// differences (greater than 3276 hours) the days, months and years fields are used as well.
//
// 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,
Expand Down Expand Up @@ -422,7 +428,8 @@ func (period Period) SecondsFloat() float32 {
// When the period specifies hours, minutes and seconds only, the result is precise.
// Also, when the period specifies whole years, months 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).
// is only an approximation (it assumes that all days are 24 hours and every year is 365.2425
// days, as per Gregorian calendar rules).
func (period Period) AddTo(t time.Time) (time.Time, bool) {
wholeYears := (period.years % 10) == 0
wholeMonths := (period.months % 10) == 0
Expand All @@ -443,8 +450,8 @@ func (period Period) AddTo(t time.Time) (time.Time, bool) {
// When the period specifies hours, minutes and seconds only, the result is precise.
// however, when the period specifies years, months 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 and a month being
// 1/12 of a that; days are all assumed to be 24 hours long.
// 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.
func (period Period) DurationApprox() time.Duration {
d, _ := period.Duration()
return d
Expand All @@ -456,8 +463,8 @@ func (period Period) DurationApprox() time.Duration {
// When the period specifies hours, minutes and seconds only, the result is precise.
// however, when the period specifies years, months 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 and a month being
// 1/12 of a that; days are all assumed to be 24 hours long.
// 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.
func (period Period) Duration() (time.Duration, bool) {
// remember that the fields are all fixed-point 1E1
tdE6 := time.Duration(totalDaysApproxE7(period) * 8640)
Expand All @@ -483,8 +490,8 @@ func totalDaysApproxE7(period Period) int64 {
}

// TotalDaysApprox gets the approximate total number of days in the period. The approximation assumes
// a year is 365.2425 days and a month is 1/12 of that. Whole multiples of 24 hours are also included
// in the calculation.
// 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) TotalDaysApprox() int {
pn := period.Normalise(false)
tdE6 := totalDaysApproxE7(pn)
Expand All @@ -493,8 +500,8 @@ func (period Period) TotalDaysApprox() 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 and a month is 1/12 of that.
// Whole multiples of 24 hours are also included in the calculation.
// 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.
func (period Period) TotalMonthsApprox() int {
pn := period.Normalise(false)
mE1 := int64(pn.years)*12 + int64(pn.months)
Expand Down
10 changes: 7 additions & 3 deletions period/period_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func TestParsePeriod(t *testing.T) {
{"PT12H", Period{0, 0, 0, 120, 0, 0}},
{"PT30M", Period{0, 0, 0, 0, 300, 0}},
{"PT25S", Period{0, 0, 0, 0, 0, 250}},
{"PT30M67.6S", Period{0, 0, 0, 0, 310, 76}},
{"P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}},
{"+P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}},
{"-P3Y6M5W4DT12H40M5S", Period{-30, -60, -390, -120, -400, -50}},
Expand Down Expand Up @@ -522,14 +523,17 @@ func TestNewOf(t *testing.T) {
testNewOf(t, time.Hour+time.Minute+time.Second, Period{0, 0, 0, 10, 10, 10}, true)
testNewOf(t, 24*time.Hour+time.Minute+time.Second, Period{0, 0, 0, 240, 10, 10}, true)
testNewOf(t, 3276*time.Hour+59*time.Minute+59*time.Second, Period{0, 0, 0, 32760, 590, 590}, true)
testNewOf(t, 30*time.Minute+67*time.Second+600*time.Millisecond, Period{0, 0, 0, 0, 310, 76}, true)

// YMD tests: must be over 3276 hours (approx 4.5 months), otherwise HMS will take care of it
// first rollover: 3276 hours
// first rollover: >3276 hours
testNewOf(t, 3277*time.Hour, Period{0, 0, 1360, 130, 0, 0}, false)
testNewOf(t, 3288*time.Hour, Period{0, 0, 1370, 0, 0, 0}, false)
testNewOf(t, 3289*time.Hour, Period{0, 0, 1370, 10, 0, 0}, false)
testNewOf(t, 3277*time.Hour, Period{0, 0, 1360, 130, 0, 0}, false)
testNewOf(t, 24*3276*time.Hour, Period{0, 0, 32760, 0, 0, 0}, false)

// second rollover: 3276 days
// second rollover: >3276 days
testNewOf(t, 24*3277*time.Hour, Period{80, 110, 200, 0, 0, 0}, false)
testNewOf(t, 3277*oneDay, Period{80, 110, 200, 0, 0, 0}, false)
testNewOf(t, 3277*oneDay+time.Hour+time.Minute+time.Second, Period{80, 110, 200, 10, 0, 0}, false)
testNewOf(t, 36525*oneDay, Period{1000, 0, 0, 0, 0, 0}, false)
Expand Down

0 comments on commit 2248ec4

Please sign in to comment.