Skip to content

Commit

Permalink
Augmented Period with AddTo(time) method. Added Period.IsPositive and…
Browse files Browse the repository at this point in the history
… clarified that Sign returns 0 for zero periods.
  • Loading branch information
Rick Beton committed Nov 27, 2018
1 parent 93168f6 commit 5959d0b
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 50 deletions.
5 changes: 3 additions & 2 deletions date.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
package date

import (
"github.com/rickb777/date/gregorian"
"github.com/rickb777/date/period"
"math"
"time"

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

// PeriodOfDays describes a period of time measured in whole days. Negative values
Expand Down
3 changes: 2 additions & 1 deletion date_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
package date

import (
"github.com/rickb777/date/period"
"runtime/debug"
"testing"
"time"

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

func same(d Date, t time.Time) bool {
Expand Down
5 changes: 3 additions & 2 deletions datetool/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ package main

import (
"fmt"
"github.com/rickb777/date"
"github.com/rickb777/date/clock"
"os"
"strconv"
"strings"

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

func printPair(a string, b interface{}) {
Expand Down
3 changes: 2 additions & 1 deletion period/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ package period
import (
"bytes"
"fmt"
"github.com/rickb777/plural"
"strings"

"github.com/rickb777/plural"
)

// Format converts the period to human-readable form using the default localisation.
Expand Down
67 changes: 50 additions & 17 deletions period/period.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ 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.25 days per year.
// are 24 hours per day, 30.4375 per month and 365.2425 days per year.
func NewOf(duration time.Duration) (p Period, precise bool) {
var sign int16 = 1
d := duration
Expand Down Expand Up @@ -203,13 +203,31 @@ func (period Period) IsZero() bool {
return period == Period{}
}

// IsPositive returns true if any field is greater than zero. By design, this also implies that
// all the other fields are greater than or equal to zero.
func (period Period) IsPositive() bool {
return period.years > 0 || period.months > 0 || period.days > 0 ||
period.hours > 0 || period.minutes > 0 || period.seconds > 0
}

// IsNegative returns true if any field is negative. By design, this also implies that
// all the fields are negative or zero.
// all the other fields are negative or zero.
func (period Period) IsNegative() bool {
return period.years < 0 || period.months < 0 || period.days < 0 ||
period.hours < 0 || period.minutes < 0 || period.seconds < 0
}

// Sign returns +1 for positive periods and -1 for negative periods. If the period is zero, it returns zero.
func (period Period) Sign() int {
if period.IsZero() {
return 0
}
if period.IsNegative() {
return -1
}
return 1
}

// OnlyYMD returns a new Period with only the year, month and day fields. The hour,
// minute and second fields are zeroed.
func (period Period) OnlyYMD() Period {
Expand Down Expand Up @@ -281,14 +299,6 @@ func (period Period) Scale(factor float32) Period {
return (&period64{y, m, d, hh, mm, ss, false}).normalise64(true).toPeriod()
}

// Sign returns +1 for positive periods and -1 for negative periods.
func (period Period) Sign() int {
if period.years < 0 || period.months < 0 || period.days < 0 || period.hours < 0 || period.minutes < 0 || period.seconds < 0 {
return -1
}
return 1
}

// Years gets the whole number of years in the period.
// The result is the number of years and does not include any other field.
func (period Period) Years() int {
Expand Down Expand Up @@ -385,11 +395,28 @@ func (period Period) SecondsFloat() float32 {
return float32(period.seconds) / 10
}

// 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.
// 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).
func (period Period) AddTo(t time.Time) (time.Time, bool) {
d, precise := period.Duration()
if !precise {
return t.Add(d), false
}

stE3 := totalSecondsE3(period)
return t.AddDate(period.Years(), period.Months(), period.Days()).Add(stE3 * time.Millisecond), true
}

// 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 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.25 days and a month being
// 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.
func (period Period) DurationApprox() time.Duration {
d, _ := period.Duration()
Expand All @@ -398,20 +425,26 @@ func (period Period) DurationApprox() time.Duration {

// 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 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.25 days and a month being
// 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.
func (period Period) Duration() (time.Duration, bool) {
// remember that the fields are all fixed-point 1E1
tdE6 := time.Duration(totalDaysApproxE7(period) * 8640)
stE3 := totalSecondsE3(period)
return tdE6*time.Microsecond + stE3*time.Millisecond, tdE6 == 0
}

func totalSecondsE3(period Period) time.Duration {
// remember that the fields are all fixed-point 1E1
// and these are divided by 1E1
hhE3 := time.Duration(period.hours) * 360000
mmE3 := time.Duration(period.minutes) * 6000
ssE3 := time.Duration(period.seconds) * 100
//fmt.Printf("y %d, m %d, d %d, hh %d, mm %d, ss %d\n", ydE6, mdE6, ddE6, hhE3, mmE3, ssE3)
stE3 := hhE3 + mmE3 + ssE3
return tdE6*time.Microsecond + stE3*time.Millisecond, tdE6 == 0
return hhE3 + mmE3 + ssE3
}

func totalDaysApproxE7(period Period) int64 {
Expand All @@ -423,7 +456,7 @@ func totalDaysApproxE7(period Period) int64 {
}

// TotalDaysApprox gets the approximate total number of days in the period. The approximation assumes
// a year is 365.25 days and a month is 1/12 of that. Whole multiples of 24 hours are also included
// 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.
func (period Period) TotalDaysApprox() int {
pn := period.Normalise(false)
Expand All @@ -433,7 +466,7 @@ func (period Period) TotalDaysApprox() int {
}

// TotalMonthsApprox gets the approximate total number of months in the period. The days component
// is included by approximately assumes a year is 365.25 days and a month is 1/12 of that.
// 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.
func (period Period) TotalMonthsApprox() int {
pn := period.Normalise(false)
Expand Down
95 changes: 76 additions & 19 deletions period/period_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
package period

import (
"github.com/rickb777/plural"
"testing"
"time"

"github.com/rickb777/plural"
)

var oneDay = 24 * time.Hour
Expand Down Expand Up @@ -210,6 +211,54 @@ func TestPeriodFloatComponents(t *testing.T) {
}
}

func TestPeriodAddToTime(t *testing.T) {
const ms = 1000000
const sec = 1000 * ms
const min = 60 * sec
const hr = 60 * min

// A conveniently round number (14 July 2017 @ 2:40am UTC)
var t0 = time.Unix(1500000000, 0)

cases := []struct {
value string
result time.Time
precise bool
}{
{"P0D", t0, true},
{"PT1S", t0.Add(sec), true},
{"PT0.1S", t0.Add(100 * ms), true},
{"-PT0.1S", t0.Add(-100 * ms), true},
{"PT3276S", t0.Add(3276 * sec), true},
{"PT1M", t0.Add(60 * sec), true},
{"PT0.1M", t0.Add(6 * sec), true},
{"PT3276M", t0.Add(3276 * min), true},
{"PT1H", t0.Add(hr), true},
{"PT0.1H", t0.Add(6 * min), true},
{"PT3276H", t0.Add(3276 * hr), true},
{"P1D", t0.Add(24 * hr), false},
{"P0.1D", t0.Add(144 * min), false},
{"P3276D", t0.Add(3276 * 24 * hr), false},
{"P1M", t0.Add(oneMonthApprox), false},
{"P0.1M", t0.Add(oneMonthApprox / 10), false},
{"P3276M", t0.Add(3276 * oneMonthApprox), false},
{"P1Y", t0.Add(oneYearApprox), false},
{"-P1Y", t0.Add(-oneYearApprox), false},
{"P3276Y", t0.Add(3276 * oneYearApprox), false}, // near the upper limit of range
{"-P3276Y", t0.Add(-3276 * oneYearApprox), false}, // near the lower limit of range
}
for i, c := range cases {
p := MustParse(c.value)
t1, prec := p.AddTo(t0)
if t1 != c.result {
t.Errorf("%d: AddTo(t) == %s %v, want %s for %s", i, t1, prec, c.result, c.value)
}
if prec != c.precise {
t.Errorf("%d: Duration() == %s %v, want %v for %s", i, t1, prec, c.precise, c.value)
}
}
}

func TestPeriodToDuration(t *testing.T) {
cases := []struct {
value string
Expand All @@ -219,6 +268,7 @@ func TestPeriodToDuration(t *testing.T) {
{"P0D", time.Duration(0), true},
{"PT1S", 1 * time.Second, true},
{"PT0.1S", 100 * time.Millisecond, true},
{"-PT0.1S", -100 * time.Millisecond, true},
{"PT3276S", 3276 * time.Second, true},
{"PT1M", 60 * time.Second, true},
{"PT0.1M", 6 * time.Second, true},
Expand Down Expand Up @@ -253,30 +303,37 @@ func TestPeriodToDuration(t *testing.T) {
}
}

func TestIsNegative(t *testing.T) {
func TestSignPotisitveNegative(t *testing.T) {
cases := []struct {
value string
expected bool
positive bool
negative bool
sign int
}{
{"P0D", false},
{"PT1S", false},
{"-PT1S", true},
{"PT1M", false},
{"-PT1M", true},
{"PT1H", false},
{"-PT1H", true},
{"P1D", false},
{"-P1D", true},
{"P1M", false},
{"-P1M", true},
{"P1Y", false},
{"-P1Y", true},
{"P0D", false, false, 0},
{"PT1S", true, false, 1},
{"-PT1S", false, true, -1},
{"PT1M", true, false, 1},
{"-PT1M", false, true, -1},
{"PT1H", true, false, 1},
{"-PT1H", false, true, -1},
{"P1D", true, false, 1},
{"-P1D", false, true, -1},
{"P1M", true, false, 1},
{"-P1M", false, true, -1},
{"P1Y", true, false, 1},
{"-P1Y", false, true, -1},
}
for i, c := range cases {
p := MustParse(c.value)
got := p.IsNegative()
if got != c.expected {
t.Errorf("%d: %v.IsNegative() == %v, want %v", i, p, got, c.expected)
if p.IsPositive() != c.positive {
t.Errorf("%d: %v.IsPositive() == %v, want %v", i, p, p.IsPositive(), c.positive)
}
if p.IsNegative() != c.negative {
t.Errorf("%d: %v.IsNegative() == %v, want %v", i, p, p.IsNegative(), c.negative)
}
if p.Sign() != c.sign {
t.Errorf("%d: %v.Sign() == %d, want %d", i, p, p.Sign(), c.sign)
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion timespan/daterange.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ package timespan

import (
"fmt"
"time"

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

const minusOneNano time.Duration = -1
Expand Down
5 changes: 3 additions & 2 deletions timespan/daterange_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ package timespan

import (
"fmt"
. "github.com/rickb777/date"
"github.com/rickb777/date/period"
"strings"
"testing"
"time"

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

var d0320 = New(2015, time.March, 20)
Expand Down
5 changes: 3 additions & 2 deletions timespan/timespan.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ package timespan

import (
"fmt"
"github.com/rickb777/date"
"github.com/rickb777/date/period"
"strings"
"time"

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

// TimestampFormat is a simple format for date & time, "2006-01-02 15:04:05".
Expand Down
5 changes: 3 additions & 2 deletions timespan/timespan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
package timespan

import (
"github.com/rickb777/date"
"testing"
"time"

"github.com/rickb777/date"
)

const zero time.Duration = 0
Expand Down Expand Up @@ -186,7 +187,7 @@ func TestTSMarshalText(t *testing.T) {
exp string
}{
{t0, time.Hour, "20150214T101314Z/PT1H"},
{t1, 2*time.Hour, "20150627T101315Z/PT2H"},
{t1, 2 * time.Hour, "20150627T101315Z/PT2H"},
{t0.In(berlin), time.Minute, "20150214T111314Z/PT1M"}, // UTC+1
{t1.In(berlin), time.Second, "20150627T121315Z/PT1S"}, // UTC+2
}
Expand Down
3 changes: 2 additions & 1 deletion view/vdate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ package view

import (
"encoding/json"
"github.com/rickb777/date"
"testing"
"time"

"github.com/rickb777/date"
)

func TestBasicFormatting(t *testing.T) {
Expand Down

0 comments on commit 5959d0b

Please sign in to comment.