pax_global_header00006660000000000000000000000064141077101640014513gustar00rootroot0000000000000052 comment=96bdc7e8708f22b2d302c3c55275f2e04956a317 golang-github-rickb777-date-1.15.3/000077500000000000000000000000001410771016400166015ustar00rootroot00000000000000golang-github-rickb777-date-1.15.3/.gitignore000066400000000000000000000004521410771016400205720ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders .idea/ _obj/ _test/ reports/ vendor/ # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof *.out golang-github-rickb777-date-1.15.3/.travis.yml000066400000000000000000000002401410771016400207060ustar00rootroot00000000000000language: go go: - '1.15' install: - go get -t -v ./... - go get github.com/mattn/goveralls script: - ./build+test.sh amd64 - ./build+test.sh 386 golang-github-rickb777-date-1.15.3/LICENSE000066400000000000000000000027241410771016400176130ustar00rootroot00000000000000Copyright (c) 2015 The Go Authors & Rick Beton. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. golang-github-rickb777-date-1.15.3/README.md000066400000000000000000000040701410771016400200610ustar00rootroot00000000000000# date [![GoDoc](https://img.shields.io/badge/api-Godoc-blue.svg)](https://pkg.go.dev/github.com/rickb777/date) [![Build Status](https://api.travis-ci.org/rickb777/date.svg?branch=master)](https://travis-ci.org/rickb777/date/builds) [![Coverage Status](https://coveralls.io/repos/rickb777/date/badge.svg?branch=master&service=github)](https://coveralls.io/github/rickb777/date?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/rickb777/date)](https://goreportcard.com/report/github.com/rickb777/date) [![Issues](https://img.shields.io/github/issues/rickb777/date.svg)](https://github.com/rickb777/date/issues) Package `date` provides functionality for working with dates. This package introduces a light-weight `Date` type that is storage-efficient and convenient for calendrical calculations and date parsing and formatting (including years outside the [0,9999] interval). It also provides * `clock.Clock` which expresses a wall-clock style hours-minutes-seconds with millisecond precision. * `period.Period` which expresses a period corresponding to the ISO-8601 form (e.g. "PT30S"). * `timespan.DateRange` which expresses a period between two dates. * `timespan.TimeSpan` which expresses a duration of time between two instants. * `view.VDate` which wraps `Date` for use in templates etc. See [package documentation](https://godoc.org/github.com/rickb777/date) for full documentation and examples. ## Installation go get -u github.com/rickb777/date or dep ensure -add github.com/rickb777/date ## Status This library has been in reliable production use for some time. Versioning follows the well-known semantic version pattern. ## Credits This package follows very closely the design of package [`time`](http://golang.org/pkg/time/) in the standard library; many of the `Date` methods are implemented using the corresponding methods of the `time.Time` type and much of the documentation is copied directly from that package. The original [Good Work](https://github.com/fxtlabs/date) on which this was based was done by Filippo Tampieri at Fxtlabs. golang-github-rickb777-date-1.15.3/build+test.sh000077500000000000000000000016731410771016400212210ustar00rootroot00000000000000#!/bin/bash -e cd "$(dirname $0)" PATH=$HOME/go/bin:$PATH unset GOPATH export GO111MODULE=on export GOARCH=${1} function v { echo echo $@ $@ } if ! type -p goveralls; then v go install github.com/mattn/goveralls fi if ! type -p shadow; then v go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow fi if ! type -p goreturns; then v go install github.com/sqs/goreturns fi echo date... v go test -v -covermode=count -coverprofile=date.out . v go tool cover -func=date.out #[ -z "$COVERALLS_TOKEN" ] || goveralls -coverprofile=date.out -service=travis-ci -repotoken $COVERALLS_TOKEN for d in clock period timespan view; do echo $d... v go test -v -covermode=count -coverprofile=$d.out ./$d v go tool cover -func=$d.out #[ -z "$COVERALLS_TOKEN" ] || goveralls -coverprofile=$d.out -service=travis-ci -repotoken $COVERALLS_TOKEN done v goreturns -l -w *.go */*.go v go vet ./... v shadow ./... v go install ./datetool golang-github-rickb777-date-1.15.3/clock/000077500000000000000000000000001410771016400176745ustar00rootroot00000000000000golang-github-rickb777-date-1.15.3/clock/clock.go000066400000000000000000000146541410771016400213300ustar00rootroot00000000000000// 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 clock specifies a time of day with resolution to the nearest millisecond. // package clock import ( "math" "time" ) // Clock specifies a time of day. It complements the existing time.Duration, applying // that to the time since midnight (on some arbitrary day in some arbitrary timezone). // The resolution is to the nearest millisecond, unlike time.Duration (which has nanosecond // resolution). // // It is not intended that Clock be used to represent periods greater than 24 hours nor // negative values. However, for such lengths of time, a fixed 24 hours per day // is assumed and a modulo operation Mod24 is provided to discard whole multiples of 24 hours. // // Clock is a type of integer (actually int32), so values can be compared and sorted as // per other integers. The constants Second, Minute, Hour and Day can be added and subtracted // with obvious outcomes. // // See https://en.wikipedia.org/wiki/ISO_8601#Times type Clock int32 // Common durations - second, minute, hour and day. const ( // Second is one second; it has a similar meaning to time.Second. Second Clock = Clock(time.Second / time.Millisecond) // Minute is one minute; it has a similar meaning to time.Minute. Minute Clock = Clock(time.Minute / time.Millisecond) // Hour is one hour; it has a similar meaning to time.Hour. Hour Clock = Clock(time.Hour / time.Millisecond) // Day is a fixed period of 24 hours. This does not take account of daylight savings, // so is not fully general. Day Clock = Clock(time.Hour * 24 / time.Millisecond) ) // Midnight is the zero value of a Clock. const Midnight Clock = 0 // Noon is at 12pm. const Noon Clock = Hour * 12 // Undefined is provided because the zero value of a Clock *is* defined (i.e. Midnight). // So a special value is chosen, which is math.MinInt32. const Undefined Clock = Clock(math.MinInt32) // New returns a new Clock with specified hour, minute, second and millisecond. func New(hour, minute, second, millisec int) Clock { hx := Clock(hour) * Hour mx := Clock(minute) * Minute sx := Clock(second) * Second return Clock(hx + mx + sx + Clock(millisec)) } // NewAt returns a new Clock with specified hour, minute, second and millisecond. func NewAt(t time.Time) Clock { hour, minute, second := t.Clock() hx := Clock(hour) * Hour mx := Clock(minute) * Minute sx := Clock(second) * Second ms := Clock(t.Nanosecond() / int(time.Millisecond)) return Clock(hx + mx + sx + ms) } // SinceMidnight returns a new Clock based on a duration since some arbitrary midnight. func SinceMidnight(d time.Duration) Clock { return Clock(d / time.Millisecond) } // DurationSinceMidnight convert a clock to a time.Duration since some arbitrary midnight. func (c Clock) DurationSinceMidnight() time.Duration { return time.Duration(c) * time.Millisecond } // Add returns a new Clock offset from this clock specified hour, minute, second and millisecond. // The parameters can be negative. // // If required, use Mod24() to correct any overflow or underflow. func (c Clock) Add(h, m, s, ms int) Clock { hx := Clock(h) * Hour mx := Clock(m) * Minute sx := Clock(s) * Second return c + hx + mx + sx + Clock(ms) } // AddDuration returns a new Clock offset from this clock by a duration. // The parameters can be negative. // // If required, use Mod24() to correct any overflow or underflow. // // AddDuration is also useful for adding period.Period values. func (c Clock) AddDuration(d time.Duration) Clock { return c + Clock(d/time.Millisecond) } // ModSubtract returns the duration between two clock times. // // If c2 is before c (i.e. c2 < c), the result is the duration computed from c - c2. // // But if c is before c2, it is assumed that c is after midnight and c2 is before midnight. The // result is the sum of the evening time from c2 to midnight with the morning time from midnight to c. // This is the same as Mod24(c - c2). func (c Clock) ModSubtract(c2 Clock) time.Duration { ms := c - c2 return ms.Mod24().DurationSinceMidnight() } // IsInOneDay tests whether a clock time is in the range 0 to 24 hours, inclusive. Inside this // range, a Clock is generally well-behaved. But outside it, there may be errors due to daylight // savings. Note that 24:00:00 is included as a special case as per ISO-8601 definition of midnight. func (c Clock) IsInOneDay() bool { return Midnight <= c && c <= Day } // IsMidnight tests whether a clock time is midnight. This is shorthand for c.Mod24() == 0. // For large values, this assumes that every day has 24 hours. func (c Clock) IsMidnight() bool { return c.Mod24() == Midnight } // Mod24 calculates the remainder vs 24 hours using Euclidean division, in which the result // will be less than 24 hours and is never negative. Note that this imposes the assumption that // every day has 24 hours (not correct when daylight saving changes in any timezone). // // https://en.wikipedia.org/wiki/Modulo_operation func (c Clock) Mod24() Clock { if Midnight <= c && c < Day { return c } if c < Midnight { q := 1 - c/Day m := c + (q * Day) if m == Day { m = Midnight } return m } q := c / Day return c - (q * Day) } // Days gets the number of whole days represented by the Clock, assuming that each day is a fixed // 24 hour period. Negative values are treated so that the range -23h59m59s to -1s is fully // enclosed in a day numbered -1, and so on. This means that the result is zero only for the // clock range 0s to 23h59m59s, for which IsInOneDay() returns true. func (c Clock) Days() int { if c < Midnight { return int(c/Day) - 1 } return int(c / Day) } // Hours gets the clock-face number of hours (calculated from the modulo time, see Mod24). func (c Clock) Hours() int { return int(clockHours(c.Mod24())) } // Minutes gets the clock-face number of minutes (calculated from the modulo time, see Mod24). // For example, for 22:35 this will return 35. func (c Clock) Minutes() int { return int(clockMinutes(c.Mod24())) } // Seconds gets the clock-face number of seconds (calculated from the modulo time, see Mod24). // For example, for 10:20:30 this will return 30. func (c Clock) Seconds() int { return int(clockSeconds(c.Mod24())) } // Millisec gets the clock-face number of milliseconds (calculated from the modulo time, see Mod24). // For example, for 10:20:30.456 this will return 456. func (c Clock) Millisec() int { return int(clockMillisec(c.Mod24())) } golang-github-rickb777-date-1.15.3/clock/clock_test.go000066400000000000000000000234521410771016400223630ustar00rootroot00000000000000// 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 clock import ( "testing" "time" ) func TestClockHoursMinutesSeconds(t *testing.T) { cases := []struct { in Clock h, m, s, ms int }{ {New(0, 0, 0, 0), 0, 0, 0, 0}, {New(1, 2, 3, 4), 1, 2, 3, 4}, {New(23, 59, 59, 999), 23, 59, 59, 999}, {New(0, 0, 0, -1), 23, 59, 59, 999}, {NewAt(time.Date(2015, 12, 4, 18, 50, 42, 173444111, time.UTC)), 18, 50, 42, 173}, } for i, x := range cases { h := x.in.Hours() m := x.in.Minutes() s := x.in.Seconds() ms := x.in.Millisec() if h != x.h || m != x.m || s != x.s || ms != x.ms { t.Errorf("%d: got %02d:%02d:%02d.%03d, want %v (%d)", i, h, m, s, ms, x.in, x.in) } } } func TestClockSinceMidnight(t *testing.T) { cases := []struct { in Clock d time.Duration }{ {New(1, 2, 3, 4), time.Hour + 2*time.Minute + 3*time.Second + 4}, {New(23, 59, 59, 999), 23*time.Hour + 59*time.Minute + 59*time.Second + 999}, } for i, x := range cases { d := x.in.DurationSinceMidnight() c2 := SinceMidnight(d) if c2 != x.in { t.Errorf("%d: got %v, want %v (%d)", i, c2, x.in, x.in) } } } func TestClockIsInOneDay(t *testing.T) { cases := []struct { in Clock want bool }{ {New(0, 0, 0, 0), true}, {New(24, 0, 0, 0), true}, {New(-24, 0, 0, 0), false}, {New(48, 0, 0, 0), false}, {New(0, 0, 0, 1), true}, {New(2, 0, 0, 1), true}, {New(-1, 0, 0, 0), false}, {New(0, 0, 0, -1), false}, } for _, x := range cases { got := x.in.IsInOneDay() if got != x.want { t.Errorf("%v got %v, want %v", x.in, x.in.IsInOneDay(), x.want) } } } func TestClockAdd(t *testing.T) { cases := []struct { h, m, s, ms int in, want Clock }{ {0, 0, 0, 0, 2 * Hour, New(2, 0, 0, 0)}, {0, 0, 0, 1, 2 * Hour, New(2, 0, 0, 1)}, {0, 0, 0, -1, 2 * Hour, New(1, 59, 59, 999)}, {0, 0, 1, 0, 2 * Hour, New(2, 0, 1, 0)}, {0, 0, -1, 0, 2 * Hour, New(1, 59, 59, 0)}, {0, 1, 0, 0, 2 * Hour, New(2, 1, 0, 0)}, {0, -1, 0, 0, 2 * Hour, New(1, 59, 0, 0)}, {1, 0, 0, 0, 2 * Hour, New(3, 0, 0, 0)}, {-1, 0, 0, 0, 2 * Hour, New(1, 0, 0, 0)}, {-2, 0, 0, 0, 2 * Hour, New(0, 0, 0, 0)}, {-2, 0, -1, -1, 2 * Hour, New(0, 0, -1, -1)}, } for i, x := range cases { got := x.in.Add(x.h, x.m, x.s, x.ms) if got != x.want { t.Errorf("%d: %d %d %d.%d: got %v, want %v", i, x.h, x.m, x.s, x.ms, got, x.want) } } } func TestClockAddDuration(t *testing.T) { cases := []struct { d time.Duration in, want Clock }{ {0, 2 * Hour, New(2, 0, 0, 0)}, {1, 2 * Hour, New(2, 0, 0, 0)}, {time.Millisecond, 2 * Hour, New(2, 0, 0, 1)}, {-time.Second, 2 * Hour, New(1, 59, 59, 0)}, {7 * time.Minute, 2 * Hour, New(2, 7, 0, 0)}, } for i, x := range cases { got := x.in.AddDuration(x.d) if got != x.want { t.Errorf("%d: %d: got %v, want %v", i, x.d, got, x.want) } } } func TestClockSubtract(t *testing.T) { cases := []struct { c1, c2 Clock want time.Duration }{ {New(1, 2, 3, 4), New(1, 2, 3, 4), 0 * time.Hour}, {New(2, 0, 0, 0), New(0, 0, 0, 0), 2 * time.Hour}, {New(0, 0, 0, 0), New(2, 0, 0, 0), 22 * time.Hour}, {New(1, 0, 0, 0), New(23, 0, 0, 0), 2 * time.Hour}, {New(23, 0, 0, 0), New(1, 0, 0, 0), 22 * time.Hour}, {New(1, 2, 3, 5), New(1, 2, 3, 4), 1 * time.Millisecond}, {New(1, 2, 3, 4), New(1, 2, 3, 5), 24*time.Hour - 1*time.Millisecond}, } for i, x := range cases { got := x.c1.ModSubtract(x.c2) if got != x.want { t.Errorf("%d: %v - %v: got %v, want %v", i, x.c1, x.c2, got, x.want) } } } func TestClockIsMidnight(t *testing.T) { cases := []struct { in Clock want bool }{ {New(0, 0, 0, 0), true}, {Day, true}, {24 * Hour, true}, {New(24, 0, 0, 0), true}, {New(-24, 0, 0, 0), true}, {New(-48, 0, 0, 0), true}, {New(48, 0, 0, 0), true}, {New(0, 0, 0, 1), false}, {New(2, 0, 0, 1), false}, {New(-1, 0, 0, 0), false}, {New(0, 0, 0, -1), false}, } for i, x := range cases { got := x.in.IsMidnight() if got != x.want { t.Errorf("%d: %v got %v, want %v, %d", i, x.in, x.in.IsMidnight(), x.want, x.in.Mod24()) } } } func TestClockMod(t *testing.T) { cases := []struct { h, want Clock }{ {0, 0}, {1 * Hour, 1 * Hour}, {2 * Hour, 2 * Hour}, {23 * Hour, 23 * Hour}, {24 * Hour, 0}, {-24 * Hour, 0}, {-48 * Hour, 0}, {25 * Hour, Hour}, {49 * Hour, Hour}, {-1 * Hour, 23 * Hour}, {-23 * Hour, Hour}, {New(0, 0, 0, 1), 1}, {New(0, 0, 1, 0), Second}, {New(0, 0, 0, -1), New(23, 59, 59, 999)}, } for i, x := range cases { clock := x.h got := clock.Mod24() if got != x.want { t.Errorf("%d: %dh: got %#v, want %#v", i, x.h, got, x.want) } } } func TestClockDays(t *testing.T) { cases := []struct { h, days int }{ {0, 0}, {1, 0}, {23, 0}, {24, 1}, {25, 1}, {48, 2}, {49, 2}, {-1, -1}, {-23, -1}, {-24, -2}, } for i, x := range cases { clock := Clock(x.h) * Hour if clock.Days() != x.days { t.Errorf("%d: %dh: got %v, want %v", i, x.h, clock.Days(), x.days) } } } func TestClockString(t *testing.T) { cases := []struct { h, m, s, ms Clock hh, hhmm, hhmmss, str, h12, hmm12, hmmss12 string }{ {0, 0, 0, 0, "00", "00:00", "00:00:00", "00:00:00.000", "12am", "12:00am", "12:00:00am"}, {0, 0, 0, 1, "00", "00:00", "00:00:00", "00:00:00.001", "12am", "12:00am", "12:00:00am"}, {0, 0, 1, 0, "00", "00:00", "00:00:01", "00:00:01.000", "12am", "12:00am", "12:00:01am"}, {0, 1, 0, 0, "00", "00:01", "00:01:00", "00:01:00.000", "12am", "12:01am", "12:01:00am"}, {1, 0, 0, 0, "01", "01:00", "01:00:00", "01:00:00.000", "1am", "1:00am", "1:00:00am"}, {1, 2, 3, 4, "01", "01:02", "01:02:03", "01:02:03.004", "1am", "1:02am", "1:02:03am"}, {11, 0, 0, 0, "11", "11:00", "11:00:00", "11:00:00.000", "11am", "11:00am", "11:00:00am"}, {12, 0, 0, 0, "12", "12:00", "12:00:00", "12:00:00.000", "12pm", "12:00pm", "12:00:00pm"}, {13, 0, 0, 0, "13", "13:00", "13:00:00", "13:00:00.000", "1pm", "1:00pm", "1:00:00pm"}, {24, 0, 0, 0, "24", "24:00", "24:00:00", "24:00:00.000", "12am", "12:00am", "12:00:00am"}, {24, 0, 0, 1, "00", "00:00", "00:00:00", "00:00:00.001", "12am", "12:00am", "12:00:00am"}, {-1, 0, 0, 0, "23", "23:00", "23:00:00", "23:00:00.000", "11pm", "11:00pm", "11:00:00pm"}, {-1, -1, -1, -1, "22", "22:58", "22:58:58", "22:58:58.999", "10pm", "10:58pm", "10:58:58pm"}, } for _, x := range cases { d := Clock(x.h*Hour + x.m*Minute + x.s*Second + x.ms) if d.Hh() != x.hh { t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.Hh(), x.hh, d) } if d.HhMm() != x.hhmm { t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.HhMm(), x.hhmm, d) } if d.HhMmSs() != x.hhmmss { t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.HhMmSs(), x.hhmmss, d) } if d.String() != x.str { t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.String(), x.str, d) } if d.Hh12() != x.h12 { t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.Hh12(), x.h12, d) } if d.HhMm12() != x.hmm12 { t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.HhMm12(), x.hmm12, d) } if d.HhMmSs12() != x.hmmss12 { t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.HhMmSs12(), x.hmmss12, d) } } } func TestClockParseGoods(t *testing.T) { cases := []struct { str string want Clock }{ {"00", New(0, 0, 0, 0)}, {"01", New(1, 0, 0, 0)}, {"23", New(23, 0, 0, 0)}, {"00:00", New(0, 0, 0, 0)}, {"00:01", New(0, 1, 0, 0)}, {"01:00", New(1, 0, 0, 0)}, {"01:02", New(1, 2, 0, 0)}, {"23:59", New(23, 59, 0, 0)}, {"0911", New(9, 11, 0, 0)}, {"1024", New(10, 24, 0, 0)}, {"2359", New(23, 59, 0, 0)}, {"00:00:00", New(0, 0, 0, 0)}, {"00:00:01", New(0, 0, 1, 0)}, {"00:01:00", New(0, 1, 0, 0)}, {"01:00:00", New(1, 0, 0, 0)}, {"01:02:03", New(1, 2, 3, 0)}, {"23:59:59", New(23, 59, 59, 0)}, {"235959", New(23, 59, 59, 0)}, {"00:00:00.000", New(0, 0, 0, 0)}, {"00:00:00.001", New(0, 0, 0, 1)}, {"00:00:01.000", New(0, 0, 1, 0)}, {"00:01:00.000", New(0, 1, 0, 0)}, {"01:00:00.000", New(1, 0, 0, 0)}, {"01:02:03.004", New(1, 2, 3, 4)}, {"01:02:03.04", New(1, 2, 3, 40)}, {"01:02:03.4", New(1, 2, 3, 400)}, {"23:59:59.999", New(23, 59, 59, 999)}, {"0am", New(0, 0, 0, 0)}, {"00am", New(0, 0, 0, 0)}, {"12am", New(0, 0, 0, 0)}, {"12pm", New(12, 0, 0, 0)}, {"12:01am", New(0, 1, 0, 0)}, {"12:01pm", New(12, 1, 0, 0)}, {"12:01:02am", New(0, 1, 2, 0)}, {"12:01:02pm", New(12, 1, 2, 0)}, {"1am", New(1, 0, 0, 0)}, {"1pm", New(13, 0, 0, 0)}, {"1:00am", New(1, 0, 0, 0)}, {"1:23am", New(1, 23, 0, 0)}, {"1:23pm", New(13, 23, 0, 0)}, {"01:23pm", New(13, 23, 0, 0)}, {"1:00:00am", New(1, 0, 0, 0)}, {"1:02:03pm", New(13, 2, 3, 0)}, {"01:02:03pm", New(13, 2, 3, 0)}, {"1:02:03.004pm", New(13, 2, 3, 4)}, {"01:02:03.004pm", New(13, 2, 3, 4)}, {"1:20:30.04pm", New(13, 20, 30, 40)}, {"1:20:30.4pm", New(13, 20, 30, 400)}, {"1:20:30.pm", New(13, 20, 30, 0)}, } for _, x := range cases { str := MustParse(x.str) if str != x.want { t.Errorf("%s, got %v, want %v", x.str, str, x.want) } } } func TestClockParseBads(t *testing.T) { cases := []struct { str string }{ {"0"}, {"0:01"}, {"0:00:01"}, {"hh"}, {"00-00"}, {"00:00-00"}, {"00:00:00-"}, {"00:00:00-0"}, {"00:00:00-00"}, {"00:00:00-000"}, {"00:mm"}, {"00:00:ss"}, {"00:00:00.xxx"}, {"01-02:03.004"}, {"01:02-03.04"}, {"01:02:03-4"}, {"12xm"}, {"12-01am"}, {"12:01-02am"}, {"ham"}, {"hham"}, {"1xm"}, {"1-00am"}, {"1:00-00am"}, {"1:02:03-4pm"}, {"1:02:03-04pm"}, {"1:02:03-004pm"}, {"1:02:03.0045pm"}, } for _, x := range cases { c, err := Parse(x.str) if err == nil { t.Errorf("%s, got %#v, want err", x.str, c) // } else { // println(err.Error()) } } } golang-github-rickb777-date-1.15.3/clock/format.go000066400000000000000000000062271410771016400215220ustar00rootroot00000000000000// 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 clock import "fmt" func clockHours(cm Clock) Clock { return (cm / Hour) } func clockHours12(cm Clock) (Clock, string) { h := clockHours(cm) if h < 1 { return 12, "am" } else if h > 12 { return h - 12, "pm" } else if h == 12 { return 12, "pm" } return h, "am" } func clockMinutes(cm Clock) Clock { return (cm % Hour) / Minute } func clockSeconds(cm Clock) Clock { return (cm % Minute) / Second } func clockMillisec(cm Clock) Clock { return cm % Second } // Hh gets the clock-face number of hours as a two-digit string. // It is calculated from the modulo time; see Mod24. // Note the special case of midnight at the end of a day is "24". func (c Clock) Hh() string { if c == Day { return "24" } cm := c.Mod24() return fmt.Sprintf("%02d", clockHours(cm)) } // HhMm gets the clock-face number of hours and minutes as a five-character ISO-8601 time string. // It is calculated from the modulo time; see Mod24. // Note the special case of midnight at the end of a day is "24:00". func (c Clock) HhMm() string { if c == Day { return "24:00" } cm := c.Mod24() return fmt.Sprintf("%02d:%02d", clockHours(cm), clockMinutes(cm)) } // HhMmSs gets the clock-face number of hours, minutes, seconds as an eight-character ISO-8601 time string. // It is calculated from the modulo time; see Mod24. // Note the special case of midnight at the end of a day is "24:00:00". func (c Clock) HhMmSs() string { if c == Day { return "24:00:00" } cm := c.Mod24() return fmt.Sprintf("%02d:%02d:%02d", clockHours(cm), clockMinutes(cm), clockSeconds(cm)) } // Hh12 gets the clock-face number of hours as a one- or two-digit string, followed by am or pm. // Remember that midnight is 12am, noon is 12pm. // It is calculated from the modulo time; see Mod24. func (c Clock) Hh12() string { cm := c.Mod24() h, sfx := clockHours12(cm) return fmt.Sprintf("%d%s", h, sfx) } // HhMm12 gets the clock-face number of hours and minutes, followed by am or pm. // Remember that midnight is 12am, noon is 12pm. // It is calculated from the modulo time; see Mod24. func (c Clock) HhMm12() string { cm := c.Mod24() h, sfx := clockHours12(cm) return fmt.Sprintf("%d:%02d%s", h, clockMinutes(cm), sfx) } // HhMmSs12 gets the clock-face number of hours, minutes and seconds, followed by am or pm. // Remember that midnight is 12am, noon is 12pm. // It is calculated from the modulo time; see Mod24. func (c Clock) HhMmSs12() string { cm := c.Mod24() h, sfx := clockHours12(cm) return fmt.Sprintf("%d:%02d:%02d%s", h, clockMinutes(cm), clockSeconds(cm), sfx) } // String gets the clock-face number of hours, minutes, seconds and milliseconds as a 12-character ISO-8601 // time string (calculated from the modulo time, see Mod24), specified to the nearest millisecond. // Note the special case of midnight at the end of a day is "24:00:00.000". func (c Clock) String() string { if c == Day { return "24:00:00.000" } cm := c.Mod24() return fmt.Sprintf("%02d:%02d:%02d.%03d", clockHours(cm), clockMinutes(cm), clockSeconds(cm), clockMillisec(cm)) } golang-github-rickb777-date-1.15.3/clock/marshal.go000066400000000000000000000017421410771016400216560ustar00rootroot00000000000000package clock import ( "errors" ) // MarshalBinary implements the encoding.BinaryMarshaler interface. func (c Clock) MarshalBinary() ([]byte, error) { enc := []byte{ byte(c >> 24), byte(c >> 16), byte(c >> 8), byte(c), } return enc, nil } // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. func (c *Clock) UnmarshalBinary(data []byte) error { if len(data) == 0 { return errors.New("Clock.UnmarshalBinary: no data") } if len(data) != 4 { return errors.New("Clock.UnmarshalBinary: invalid length") } *c = Clock(data[3]) | Clock(data[2])<<8 | Clock(data[1])<<16 | Clock(data[0])<<24 return nil } // MarshalText implements the encoding.TextMarshaler interface. func (c Clock) MarshalText() ([]byte, error) { return []byte(c.String()), nil } // UnmarshalText implements the encoding.TextUnmarshaler interface. func (c *Clock) UnmarshalText(data []byte) (err error) { clock, err := Parse(string(data)) if err == nil { *c = clock } return err } golang-github-rickb777-date-1.15.3/clock/marshal_test.go000066400000000000000000000074101410771016400227130ustar00rootroot00000000000000package clock import ( "bytes" "encoding/gob" "encoding/json" "strings" "testing" ) func TestGobEncoding(t *testing.T) { var b bytes.Buffer encoder := gob.NewEncoder(&b) decoder := gob.NewDecoder(&b) cases := []Clock{ New(-1, -1, -1, -1), New(0, 0, 0, 0), New(12, 40, 40, 80), New(13, 55, 0, 20), New(16, 20, 0, 0), New(20, 60, 59, 59), New(24, 0, 0, 0), New(24, 0, 0, 1), } for _, c := range cases { var clock Clock err := encoder.Encode(&c) if err != nil { t.Errorf("Gob(%v) encode error %v", c, err) } else { err = decoder.Decode(&clock) if err != nil { t.Errorf("Gob(%v) decode error %v", c, err) } else if clock != c { t.Errorf("Gob(%v) decode got %v", c, clock) } } } } func TestJSONMarshalling(t *testing.T) { cases := []struct { value Clock want string }{ {New(-1, -1, -1, -1), `"22:58:58.999"`}, {New(0, 0, 0, 0), `"00:00:00.000"`}, {New(12, 40, 40, 80), `"12:40:40.080"`}, {New(13, 55, 0, 20), `"13:55:00.020"`}, {New(16, 20, 0, 0), `"16:20:00.000"`}, {New(20, 60, 59, 59), `"21:00:59.059"`}, {New(24, 0, 0, 0), `"24:00:00.000"`}, {New(24, 0, 0, 1), `"00:00:00.001"`}, } for _, c := range cases { bb, err := json.Marshal(c.value) if err != nil { t.Errorf("JSON(%v) marshal error %v", c, err) } else if string(bb) != c.want { t.Errorf("JSON(%v) == %v, want %v", c.value, string(bb), c.want) } } } func TestJSONUnmarshalling(t *testing.T) { cases := []struct { values []string want Clock }{ {[]string{`"22:58:58.999"`, `"10:58:58.999pm"`}, New(-1, -1, -1, -1)}, {[]string{`"00:00:00.000"`, `"00:00:00.000AM"`}, New(0, 0, 0, 0)}, {[]string{`"12:40:40.080"`, `"12:40:40.080PM"`}, New(12, 40, 40, 80)}, {[]string{`"13:55:00.020"`, `"01:55:00.020PM"`}, New(13, 55, 0, 20)}, {[]string{`"16:20:00.000"`, `"04:20:00.000pm"`}, New(16, 20, 0, 0)}, {[]string{`"21:00:59.059"`, `"09:00:59.059PM"`}, New(20, 60, 59, 59)}, {[]string{`"24:00:00.000"`, `"00:00:00.000am"`}, New(24, 0, 0, 0)}, {[]string{`"00:00:00.001"`, `"00:00:00.001AM"`}, New(24, 0, 0, 1)}, } for _, c := range cases { for _, v := range c.values { var clock Clock err := json.Unmarshal([]byte(v), &clock) if err != nil { t.Errorf("JSON(%v) unmarshal error %v", v, err) } else if c.want.Mod24() != clock.Mod24() { t.Errorf("JSON(%v) == %v, want %v", v, clock, c.want) } } } } func TestBinaryMarshalling(t *testing.T) { cases := []Clock{ New(-1, -1, -1, -1), New(0, 0, 0, 0), New(12, 40, 40, 80), New(13, 55, 0, 20), New(16, 20, 0, 0), New(20, 60, 59, 59), New(24, 0, 0, 0), New(24, 0, 0, 1), } for _, c := range cases { bb, err := c.MarshalBinary() if err != nil { t.Errorf("Binary(%v) marshal error %v", c, err) } else { var clock Clock err = clock.UnmarshalBinary(bb) if err != nil { t.Errorf("Binary(% v) unmarshal error %v", c, err) } else if clock.Mod24() != c.Mod24() { t.Errorf("Binary(%v) unmarshal got %v", c, clock) } } } } func TestBinaryUnmarshallingErrors(t *testing.T) { var c Clock err1 := c.UnmarshalBinary([]byte{}) if err1 == nil { t.Errorf("unmarshal no empty data error") } err2 := c.UnmarshalBinary([]byte("12345")) if err2 == nil { t.Errorf("unmarshal no wrong length error") } } func TestInvalidClockText(t *testing.T) { cases := []struct { value string want string }{ {`not-a-clock`, `clock.Clock: cannot parse not-a-clock`}, {`00:50:100.0`, `clock.Clock: cannot parse 00:50:100.0`}, {`24:00:00.0pM`, `clock.Clock: cannot parse 24:00:00.0pM: strconv.Atoi: parsing "0pM": invalid syntax`}, } for _, c := range cases { var clock Clock err := clock.UnmarshalText([]byte(c.value)) if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("InvalidText(%v) == %v, want %v", c.value, err, c.want) } } } golang-github-rickb777-date-1.15.3/clock/parse.go000066400000000000000000000104541410771016400213410ustar00rootroot00000000000000// 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 clock import ( "fmt" "runtime" "strconv" "strings" ) // MustParse is as per 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 MustParse(hms string) Clock { t, err := Parse(hms) if err != nil { panic(err) } return t } // Parse converts a string representation to a Clock. Acceptable representations // are as per ISO-8601 - see https://en.wikipedia.org/wiki/ISO_8601#Times // // Also, conventional AM- and PM-based strings are parsed, such as "2am", "2:45pm". // Remember that 12am is midnight and 12pm is noon. func Parse(hms string) (clock Clock, err error) { if strings.HasSuffix(hms, "am") || strings.HasSuffix(hms, "AM") { return parseAmPm(hms, 0) } else if strings.HasSuffix(hms, "pm") || strings.HasSuffix(hms, "PM") { return parseAmPm(hms, 12) } return parseISO(hms) } func parseISO(hms string) (clock Clock, err error) { switch len(hms) { case 2: // HH return parseClockParts(hms, hms, "", "", "", 0, 0) case 4: // HHMM return parseClockParts(hms, hms[:2], hms[2:], "", "", 0, 0) case 5: // HH:MM if hms[2] != ':' { return 0, parseError(hms, nil) } return parseClockParts(hms, hms[:2], hms[3:], "", "", 0, 0) case 6: // HHMMSS return parseClockParts(hms, hms[:2], hms[2:4], hms[4:], "", 0, 0) case 8: // HH:MM:SS if hms[2] != ':' || hms[5] != ':' { return 0, parseError(hms, nil) } return parseClockParts(hms, hms[:2], hms[3:5], hms[6:], "", 0, 0) case 9, 10: // HH:MM:SS.0 if hms[2] != ':' || hms[5] != ':' || hms[8] != '.' { return 0, parseError(hms, nil) } return parseClockParts(hms, hms[:2], hms[3:5], hms[6:8], hms[9:]+"00", 0, 0) case 11: // HH:MM:SS.00 if hms[2] != ':' || hms[5] != ':' || hms[8] != '.' { return 0, parseError(hms, nil) } return parseClockParts(hms, hms[:2], hms[3:5], hms[6:8], hms[9:]+"0", 0, 0) case 12: // HH:MM:SS.000 if hms[2] != ':' || hms[5] != ':' || hms[8] != '.' { return 0, parseError(hms, nil) } return parseClockParts(hms, hms[:2], hms[3:5], hms[6:8], hms[9:], 0, 0) } return 0, parseError(hms, nil) } func parseAmPm(hms string, offset int) (clock Clock, err error) { n := len(hms) switch len(hms) { case 3: // Ham return parseClockParts(hms, "0"+hms[:1], "", "", "", 12, offset) case 4: // HHam return parseClockParts(hms, hms[:2], "", "", "", 12, offset) } colon := strings.IndexByte(hms, ':') if colon < 0 { return 0, parseError(hms, nil) } h := hms[:colon] rest := hms[colon+1 : n-2] switch len(rest) { case 2: // MM return parseClockParts(hms, h, rest, "", "", 12, offset) case 5: // MM:SS if rest[2] != ':' { return 0, parseError(hms, nil) } return parseClockParts(hms, h, rest[:2], rest[3:], "", 12, offset) case 6, 7: // MM:SS.0xm if rest[2] != ':' || rest[5] != '.' { return 0, parseError(hms, nil) } return parseClockParts(hms, h, rest[:2], rest[3:5], rest[6:]+"00", 12, offset) case 8: // MM:SS.00xm if rest[2] != ':' || rest[5] != '.' { return 0, parseError(hms, nil) } return parseClockParts(hms, h, rest[:2], rest[3:5], rest[6:]+"0", 12, offset) case 9: // MM:SS.000xm if rest[2] != ':' || rest[5] != '.' { return 0, parseError(hms, nil) } return parseClockParts(hms, h, rest[:2], rest[3:5], rest[6:], 12, offset) } return 0, parseError(hms, nil) } func parseClockParts(hms, hh, mm, ss, mmms string, mod, offset int) (clock Clock, err error) { h := 0 m := 0 s := 0 ms := 0 if hh != "" { h, err = strconv.Atoi(hh) if err != nil { return 0, parseError(hms, err) } } if mm != "" { m, err = strconv.Atoi(mm) if err != nil { return 0, parseError(hms, err) } } if ss != "" { s, err = strconv.Atoi(ss) if err != nil { return 0, parseError(hms, err) } } if mmms != "" { ms, err = strconv.Atoi(mmms) if err != nil { return 0, parseError(hms, err) } } if mod > 0 { h = h % mod } return New(h+offset, m, s, ms), nil } func parseError(hms string, err error) error { _, _, line, _ := runtime.Caller(1) if err != nil { return fmt.Errorf("parse.go:%d: clock.Clock: cannot parse %s: %v", line, hms, err) } return fmt.Errorf("parse.go:%d: clock.Clock: cannot parse %s", line, hms) } golang-github-rickb777-date-1.15.3/clock/sql.go000066400000000000000000000015331410771016400210240ustar00rootroot00000000000000package clock import ( "database/sql/driver" "fmt" "time" ) // Scan parses some value. It implements sql.Scanner, // https://golang.org/pkg/database/sql/#Scanner func (c *Clock) Scan(value interface{}) (err error) { if value == nil { return nil } return c.scanAny(value) } func (c *Clock) scanAny(value interface{}) (err error) { err = nil switch value.(type) { case int64: *c = Clock(value.(int64)) case []byte: *c, err = Parse(string(value.([]byte))) case string: *c, err = Parse(value.(string)) case time.Time: *c = NewAt(value.(time.Time)) default: err = fmt.Errorf("%T %+v is not a meaningful clock", value, value) } return } // Value converts the value to an int64. It implements driver.Valuer, // https://golang.org/pkg/database/sql/driver/#Valuer func (c Clock) Value() (driver.Value, error) { return int64(c), nil } golang-github-rickb777-date-1.15.3/clock/sql_test.go000066400000000000000000000030371410771016400220640ustar00rootroot00000000000000package clock import ( "database/sql/driver" "testing" "time" ) func TestClockScan(t *testing.T) { now := time.Now() cases := []struct { v interface{} expected Clock }{ {int64(New(-1, -1, -1, -1)), New(-1, -1, -1, -1)}, {int64(New(10, 60, 10, 0)), New(10, 60, 10, 0)}, {int64(New(24, 10, 0, 10)), New(0, 10, 0, 10)}, {"12:00:00.400", New(12, 0, 0, 400)}, {"01:40:50.000pm", New(13, 40, 50, 0)}, {"4:20:00.000pm", New(16, 20, 0, 0)}, {[]byte("23:60:60.000"), New(0, 1, 0, 0)}, {now, NewAt(now)}, } for i, c := range cases { var clock Clock e := clock.Scan(c.v) if e != nil { t.Errorf("%d: Got %v for %d", i, e, c.expected) } else if clock.Mod24() != c.expected.Mod24() { t.Errorf("%d: Got %v, want %d", i, clock, c.expected) } var d driver.Valuer = clock q, e := d.Value() if e != nil { t.Errorf("%d: Got %v for %d", i, e, c.expected) } else if Clock(q.(int64)).Mod24() != c.expected.Mod24() { t.Errorf("%d: Got %v, want %d", i, q, c.expected) } } } func TestClockScanWithJunk(t *testing.T) { cases := []struct { v interface{} expected string }{ {true, "bool true is not a meaningful clock"}, {false, "bool false is not a meaningful clock"}, } for i, c := range cases { var clock Clock e := clock.Scan(c.v) if e.Error() != c.expected { t.Errorf("%d: Got %q, want %q", i, e.Error(), c.expected) } } } func TestClockScanWithNil(t *testing.T) { var r *Clock e := r.Scan(nil) if e != nil { t.Errorf("Got %v", e) } if r != nil { t.Errorf("Got %v", r) } } golang-github-rickb777-date-1.15.3/coverage.sh000077500000000000000000000017361410771016400207420ustar00rootroot00000000000000#!/bin/bash -e # Developer tool to run the tests and obtain HTML coverage reports. DIR=$PWD DOT="$(dirname $0)" cd $DOT TOP=$PWD # install Goveralls if absent if ! type -p goveralls; then echo go install github.com/mattn/goveralls go install github.com/mattn/goveralls fi mkdir -p reports rm -f reports/*.html coverage?*.* for file in $(find . -type f -name \*_test.go | fgrep -v vendor/); do dirname $file >> coverage$$.tmp done sort coverage$$.tmp | uniq | tee coverage$$.dirs for pkg in $(cat coverage$$.dirs); do name=$(echo $pkg | sed 's#^./##' | sed 's#/#-#g') [ "$pkg" = "." ] && name=$(basename $PWD) echo $pkg becomes $name go test -v -coverprofile coverage$$.data $pkg if [ -f coverage$$.data ]; then go tool cover -html coverage$$.data -o reports/$name.html unlink coverage$$.data fi done rm -f coverage$$.tmp coverage$$.dirs if [ -n "$(type -p chromium-browser)" ]; then chromium-browser reports/*.html >/dev/null & else ls -lh reports/ fi golang-github-rickb777-date-1.15.3/date.go000066400000000000000000000215251410771016400200520ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package date import ( "math" "time" "github.com/rickb777/date/gregorian" "github.com/rickb777/date/period" ) // PeriodOfDays describes a period of time measured in whole days. Negative values // indicate days earlier than some mark. type PeriodOfDays int32 // ZeroDays is the named zero value for PeriodOfDays. const ZeroDays PeriodOfDays = 0 // A Date represents a date under the (proleptic) Gregorian calendar as // used by ISO 8601. This calendar uses astronomical year numbering, // so it includes a year 0 and represents earlier years as negative numbers // (i.e. year 0 is 1 BC; year -1 is 2 BC, and so on). // // A Date value requires 4 bytes of storage and can represent dates from // Tue, 23 Jun -5,877,641 (5,877,642 BC) to Fri, 11 Jul 5,881,580. // Dates outside that range will "wrap around". // // Programs using dates should typically store and pass them as values, // not pointers. That is, date variables and struct fields should be of // type date.Date, not *date.Date. A Date value can be used by // multiple goroutines simultaneously. // // Date values can be compared using the Before, After, and Equal methods // as well as the == and != operators. // // The Sub method subtracts two dates, returning the number of days between // them. The Add method adds a Date and a number of days, producing a Date. // // The zero value of type Date is Thursday, January 1, 1970 (called 'the // epoch'), based on Unix convention. As this date is unlikely to come up in // practice, the IsZero method gives a simple way of detecting a date that // has not been initialized explicitly. // // The first official date of the Gregorian calendar was Friday, October 15th // 1582, quite unrelated to the epoch used here. The Date type does not // distinguish between official Gregorian dates and earlier proleptic dates, // which can also be represented when needed. // type Date struct { day PeriodOfDays // day gives the number of days elapsed since date zero. } // New returns the Date value corresponding to the given year, month, and day. // // The month and day may be outside their usual ranges and will be normalized // during the conversion. func New(year int, month time.Month, day int) Date { t := time.Date(year, month, day, 12, 0, 0, 0, time.UTC) return Date{encode(t)} } // NewAt returns the Date value corresponding to the given time. // Note that the date is computed relative to the time zone specified by // the given Time value. func NewAt(t time.Time) Date { return Date{encode(t)} } // NewOfDays returns the Date value corresponding to the given period since the // epoch (1st January 1970), which may be negative. func NewOfDays(p PeriodOfDays) Date { return Date{p} } // Date returns the Date value corresponding to the given period since the // epoch (1st January 1970), which may be negative. func (p PeriodOfDays) Date() Date { return Date{p} } // Today returns today's date according to the current local time. func Today() Date { t := time.Now() return Date{encode(t)} } // TodayUTC returns today's date according to the current UTC time. func TodayUTC() Date { t := time.Now().UTC() return Date{encode(t)} } // TodayIn returns today's date according to the current time relative to // the specified location. func TodayIn(loc *time.Location) Date { t := time.Now().In(loc) return Date{encode(t)} } // Min returns the smallest representable date. func Min() Date { return Date{day: PeriodOfDays(math.MinInt32)} } // Max returns the largest representable date. func Max() Date { return Date{day: PeriodOfDays(math.MaxInt32)} } // UTC returns a Time value corresponding to midnight on the given date, // UTC time. Note that midnight is the beginning of the day rather than the end. func (d Date) UTC() time.Time { return decode(d.day) } // Local returns a Time value corresponding to midnight on the given date, // local time. Note that midnight is the beginning of the day rather than the end. func (d Date) Local() time.Time { return d.In(time.Local) } // In returns a Time value corresponding to midnight on the given date, // relative to the specified time zone. Note that midnight is the beginning // of the day rather than the end. func (d Date) In(loc *time.Location) time.Time { t := decode(d.day).In(loc) _, offset := t.Zone() return t.Add(time.Duration(-offset) * time.Second) } // Date returns the year, month, and day of d. // The first day of the month is 1. func (d Date) Date() (year int, month time.Month, day int) { t := decode(d.day) return t.Date() } // LastDayOfMonth returns the last day of the month specified by d. // The first day of the month is 1. func (d Date) LastDayOfMonth() int { y, m, _ := d.Date() return DaysIn(y, m) } // Day returns the day of the month specified by d. // The first day of the month is 1. func (d Date) Day() int { t := decode(d.day) return t.Day() } // Month returns the month of the year specified by d. func (d Date) Month() time.Month { t := decode(d.day) return t.Month() } // Year returns the year specified by d. func (d Date) Year() int { t := decode(d.day) return t.Year() } // YearDay returns the day of the year specified by d, in the range [1,365] for // non-leap years, and [1,366] in leap years. func (d Date) YearDay() int { t := decode(d.day) return t.YearDay() } // Weekday returns the day of the week specified by d. func (d Date) Weekday() time.Weekday { // Date zero, January 1, 1970, fell on a Thursday wdayZero := time.Thursday // Taking into account potential for overflow and negative offset return time.Weekday((int32(wdayZero) + int32(d.day)%7 + 7) % 7) } // ISOWeek returns the ISO 8601 year and week number in which d occurs. // Week ranges from 1 to 53. Jan 01 to Jan 03 of year n might belong to // week 52 or 53 of year n-1, and Dec 29 to Dec 31 might belong to week 1 // of year n+1. func (d Date) ISOWeek() (year, week int) { t := decode(d.day) return t.ISOWeek() } // IsZero reports whether t represents the zero date. func (d Date) IsZero() bool { return d.day == 0 } // Equal reports whether d and u represent the same date. func (d Date) Equal(u Date) bool { return d.day == u.day } // Before reports whether the date d is before u. func (d Date) Before(u Date) bool { return d.day < u.day } // After reports whether the date d is after u. func (d Date) After(u Date) bool { return d.day > u.day } // Min returns the earlier of two dates. func (d Date) Min(u Date) Date { if d.day > u.day { return u } return d } // Max returns the later of two dates. func (d Date) Max(u Date) Date { if d.day < u.day { return u } return d } // Add returns the date d plus the given number of days. The parameter may be negative. func (d Date) Add(days PeriodOfDays) Date { return Date{d.day + days} } // AddDate returns the date corresponding to adding the given number of years, // months, and days to d. For example, AddData(-1, 2, 3) applied to // January 1, 2011 returns March 4, 2010. // // AddDate normalizes its result in the same way that Date does, // so, for example, adding one month to October 31 yields // December 1, the normalized form for November 31. // // The addition of all fields is performed before normalisation of any; this can affect // the result. For example, adding 0y 1m 3d to September 28 gives October 31 (not // November 1). func (d Date) AddDate(years, months, days int) Date { t := decode(d.day).AddDate(years, months, days) return Date{encode(t)} } // AddPeriod returns the date corresponding to adding the given period. If the // period's fields are be negative, this results in an earlier date. // // 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. // // See the description for AddDate. func (d Date) AddPeriod(delta period.Period) Date { return d.AddDate(delta.Years(), delta.Months(), delta.Days()) } // Sub returns d-u as the number of days between the two dates. func (d Date) Sub(u Date) (days PeriodOfDays) { return d.day - u.day } // DaysSinceEpoch returns the number of days since the epoch (1st January 1970), which may be negative. func (d Date) DaysSinceEpoch() (days PeriodOfDays) { return d.day } // IsLeap simply tests whether a given year is a leap year, using the Gregorian calendar algorithm. func IsLeap(year int) bool { return gregorian.IsLeap(year) } // DaysIn gives the number of days in a given month, according to the Gregorian calendar. func DaysIn(year int, month time.Month) int { return gregorian.DaysIn(year, month) } golang-github-rickb777-date-1.15.3/date_test.go000066400000000000000000000205001410771016400211010ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package date import ( "runtime/debug" "testing" "time" "github.com/rickb777/date/period" ) func same(d Date, t time.Time) bool { yd, wd := d.ISOWeek() yt, wt := t.ISOWeek() return d.Year() == t.Year() && d.Month() == t.Month() && d.Day() == t.Day() && d.Weekday() == t.Weekday() && d.YearDay() == t.YearDay() && yd == yt && wd == wt } func TestNew(t *testing.T) { cases := []string{ "0000-01-01T00:00:00+00:00", "0001-01-01T00:00:00+00:00", "1614-01-01T01:02:03+04:00", "1970-01-01T00:00:00+00:00", "1815-12-10T05:06:07+00:00", "1901-09-10T00:00:00-05:00", "1998-09-01T00:00:00-08:00", "2000-01-01T00:00:00+00:00", "9999-12-31T00:00:00+00:00", } for _, c := range cases { tIn, err := time.Parse(time.RFC3339, c) if err != nil { t.Errorf("New(%v) cannot parse input: %v", c, err) continue } dOut := New(tIn.Year(), tIn.Month(), tIn.Day()) if !same(dOut, tIn) { t.Errorf("New(%v) == %v, want date of %v", c, dOut, tIn) } dOut = NewAt(tIn) if !same(dOut, tIn) { t.Errorf("NewAt(%v) == %v, want date of %v", c, dOut, tIn) } } } func TestDaysSinceEpoch(t *testing.T) { zero := Date{}.DaysSinceEpoch() if zero != 0 { t.Errorf("Non zero %v", zero) } today := Today() days := today.DaysSinceEpoch() copy1 := NewOfDays(days) copy2 := days.Date() if today != copy1 || days == 0 { t.Errorf("Today == %v, want date of %v", today, copy1) } if today != copy2 || days == 0 { t.Errorf("Today == %v, want date of %v", today, copy2) } } func TestToday(t *testing.T) { today := Today() now := time.Now() if !same(today, now) { t.Errorf("Today == %v, want date of %v", today, now) } today = TodayUTC() now = time.Now().UTC() if !same(today, now) { t.Errorf("TodayUTC == %v, want date of %v", today, now) } cases := []int{-10, -5, -3, 0, 1, 4, 8, 12} for _, c := range cases { location := time.FixedZone("zone", c*60*60) today = TodayIn(location) now = time.Now().In(location) if !same(today, now) { t.Errorf("TodayIn(%v) == %v, want date of %v", c, today, now) } } } func TestTime(t *testing.T) { cases := []struct { d Date }{ {New(-1234, time.February, 5)}, {New(0, time.April, 12)}, {New(1, time.January, 1)}, {New(1946, time.February, 4)}, {New(1970, time.January, 1)}, {New(1976, time.April, 1)}, {New(1999, time.December, 1)}, {New(1111111, time.June, 21)}, } zones := []int{-12, -10, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 8, 12} for _, c := range cases { d := c.d tUTC := d.UTC() if !same(d, tUTC) { t.Errorf("TimeUTC(%v) == %v, want date part %v", d, tUTC, d) } if tUTC.Location() != time.UTC { t.Errorf("TimeUTC(%v) == %v, want %v", d, tUTC.Location(), time.UTC) } tLocal := d.Local() if !same(d, tLocal) { t.Errorf("TimeLocal(%v) == %v, want date part %v", d, tLocal, d) } if tLocal.Location() != time.Local { t.Errorf("TimeLocal(%v) == %v, want %v", d, tLocal.Location(), time.Local) } for _, z := range zones { location := time.FixedZone("zone", z*60*60) tInLoc := d.In(location) if !same(d, tInLoc) { t.Errorf("TimeIn(%v) == %v, want date part %v", d, tInLoc, d) } if tInLoc.Location() != location { t.Errorf("TimeIn(%v) == %v, want %v", d, tInLoc.Location(), location) } } } } func TestPredicates(t *testing.T) { // The list of case dates must be sorted in ascending order cases := []struct { d Date }{ {New(-1234, time.February, 5)}, {New(0, time.April, 12)}, {New(1, time.January, 1)}, {New(1946, time.February, 4)}, {New(1970, time.January, 1)}, {New(1976, time.April, 1)}, {New(1999, time.December, 1)}, {New(1111111, time.June, 21)}, } for i, ci := range cases { di := ci.d for j, cj := range cases { dj := cj.d testPredicate(t, di, dj, di.Equal(dj), i == j, "Equal") testPredicate(t, di, dj, di.Before(dj), i < j, "Before") testPredicate(t, di, dj, di.After(dj), i > j, "After") testPredicate(t, di, dj, di == dj, i == j, "==") testPredicate(t, di, dj, di != dj, i != j, "!=") } } // Test IsZero zero := Date{} if !zero.IsZero() { t.Errorf("IsZero(%v) == false, want true", zero) } today := Today() if today.IsZero() { t.Errorf("IsZero(%v) == true, want false", today) } } func testPredicate(t *testing.T, di, dj Date, p, q bool, m string) { if p != q { t.Errorf("%s(%v, %v) == %v, want %v\n%v", m, di, dj, p, q, debug.Stack()) } } func TestArithmetic(t *testing.T) { cases := []struct { d Date }{ {New(-1234, time.February, 5)}, {New(0, time.April, 12)}, {New(1, time.January, 1)}, {New(1946, time.February, 4)}, {New(1970, time.January, 1)}, {New(1976, time.April, 1)}, {New(1999, time.December, 1)}, {New(1111111, time.June, 21)}, } offsets := []PeriodOfDays{-1000000, -9999, -555, -99, -22, -1, 0, 1, 22, 99, 555, 9999, 1000000} for _, c := range cases { di := c.d for _, days := range offsets { dj := di.Add(days) days2 := dj.Sub(di) if days2 != days { t.Errorf("AddSub(%v,%v) == %v, want %v", di, days, days2, days) } d3 := dj.Add(-days) if d3 != di { t.Errorf("AddNeg(%v,%v) == %v, want %v", di, days, d3, di) } eMin1 := min(di.day, dj.day) aMin1 := di.Min(dj) if aMin1.day != eMin1 { t.Errorf("%v.Max(%v) is %s", di, dj, aMin1) } eMax1 := max(di.day, dj.day) aMax1 := di.Max(dj) if aMax1.day != eMax1 { t.Errorf("%v.Max(%v) is %s", di, dj, aMax1) } } } } func TestAddDate(t *testing.T) { cases := []struct { d Date years, months, days int expected Date }{ {New(1970, time.January, 1), 1, 2, 3, New(1971, time.March, 4)}, {New(1999, time.September, 28), 6, 4, 2, New(2006, time.January, 30)}, {New(1999, time.September, 28), 0, 0, 3, New(1999, time.October, 1)}, {New(1999, time.September, 28), 0, 1, 3, New(1999, time.October, 31)}, } for _, c := range cases { di := c.d dj := di.AddDate(c.years, c.months, c.days) if dj != c.expected { t.Errorf("%v AddDate(%v,%v,%v) == %v, want %v", di, c.years, c.months, c.days, dj, c.expected) } dk := dj.AddDate(-c.years, -c.months, -c.days) if dk != di { t.Errorf("%v AddDate(%v,%v,%v) == %v, want %v", dj, -c.years, -c.months, -c.days, dk, di) } } } func TestAddPeriod(t *testing.T) { cases := []struct { in Date delta period.Period expected Date }{ {New(1970, time.January, 1), period.NewYMD(0, 0, 0), New(1970, time.January, 1)}, {New(1971, time.January, 1), period.NewYMD(10, 0, 0), New(1981, time.January, 1)}, {New(1972, time.January, 1), period.NewYMD(0, 10, 0), New(1972, time.November, 1)}, {New(1972, time.January, 1), period.NewYMD(0, 24, 0), New(1974, time.January, 1)}, {New(1973, time.January, 1), period.NewYMD(0, 0, 10), New(1973, time.January, 11)}, {New(1973, time.January, 1), period.NewYMD(0, 0, 365), New(1974, time.January, 1)}, {New(1974, time.January, 1), period.NewHMS(1, 2, 3), New(1974, time.January, 1)}, // note: the period is not normalised so the HMS is ignored even though it's more than one day {New(1975, time.January, 1), period.NewHMS(24, 2, 3), New(1975, time.January, 1)}, } for i, c := range cases { out := c.in.AddPeriod(c.delta) if out != c.expected { t.Errorf("%d: %v.AddPeriod(%v) == %v, want %v", i, c.in, c.delta, out, c.expected) } } } func min(a, b PeriodOfDays) PeriodOfDays { if a < b { return a } return b } func max(a, b PeriodOfDays) PeriodOfDays { if a > b { return a } return b } // See main testin in period_test.go func TestIsLeap(t *testing.T) { cases := []struct { year int expected bool }{ {2000, true}, {2001, false}, } for _, c := range cases { got := IsLeap(c.year) if got != c.expected { t.Errorf("TestIsLeap(%d) == %v, want %v", c.year, got, c.expected) } } } func TestDaysIn(t *testing.T) { cases := []struct { year int month time.Month expected int }{ {2000, time.January, 31}, {2000, time.February, 29}, {2001, time.February, 28}, {2001, time.April, 30}, } for _, c := range cases { got1 := DaysIn(c.year, c.month) if got1 != c.expected { t.Errorf("DaysIn(%d, %d) == %v, want %v", c.year, c.month, got1, c.expected) } d := New(c.year, c.month, 1) got2 := d.LastDayOfMonth() if got2 != c.expected { t.Errorf("DaysIn(%d) == %v, want %v", c.year, got2, c.expected) } } } golang-github-rickb777-date-1.15.3/datetool/000077500000000000000000000000001410771016400204145ustar00rootroot00000000000000golang-github-rickb777-date-1.15.3/datetool/main.go000066400000000000000000000043001410771016400216640ustar00rootroot00000000000000// 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. // This tool prints equivalences between the string representation and the internal numerical // representation for dates and clocks. // package main import ( "fmt" "os" "strconv" "github.com/rickb777/date" "github.com/rickb777/date/clock" "golang.org/x/text/language" "golang.org/x/text/message" ) func usage() { fmt.Printf("Usage: %s [-t] number | date | time\n\n", os.Args[0]) fmt.Printf(" -t: terse output\n") fmt.Printf(" date: [+-]yyyy/mm/dd | yyyy.mm.dd | dd/mm/yyyy | dd.mm.yyyy\n") fmt.Printf(" time: e.g. 11:15:20 | 2:45pm | 1:15:10.101\n") os.Exit(0) } var titled = false var terse = false var success = false var printer = message.NewPrinter(language.English) func sprintf(num interface{}) string { if terse { return fmt.Sprintf("%d", num) } else { return printer.Sprintf("%d", num) } } func title() { if !terse && !titled { titled = true fmt.Printf("%-15s %-15s %-15s %s\n", "input", "number", "clock", "date") fmt.Printf("%-15s %-15s %-15s %s\n", "-----", "------", "-----", "----") } } func printArg(arg string) { i, err := strconv.ParseInt(arg, 10, 64) if err == nil { title() d := date.NewOfDays(date.PeriodOfDays(i)) c := clock.Clock(i) fmt.Printf("%-15s %-15s %-15s %-12s %s\n", arg, sprintf(i), c, d, d.Weekday()) success = true return } d, e1 := date.AutoParse(arg) if e1 == nil { title() fmt.Printf("%-15s %-15s %15s %-12s %s\n", arg, sprintf(d.DaysSinceEpoch()), "", d, d.Weekday()) success = true } c, err := clock.Parse(arg) if err == nil { title() fmt.Printf("%-15s %-15s %s\n", arg, sprintf(c), c) success = true } } func main() { argsWithoutProg := os.Args[1:] if len(argsWithoutProg) == 0 { usage() } if len(argsWithoutProg) > 0 && argsWithoutProg[0] == "-t" { terse = true argsWithoutProg = argsWithoutProg[1:] } for _, arg := range argsWithoutProg { printArg(arg) } if !success { usage() } if titled { fmt.Printf("\n# dates are counted using days since Thursday 1st Jan 1970\n") fmt.Printf("# clock operates via milliseconds since midnight\n") } } golang-github-rickb777-date-1.15.3/doc.go000066400000000000000000000031731410771016400177010ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package date provides functionality for working with dates. // It implements a light-weight Date type that is storage-efficient // and convenient for calendrical calculations and date parsing and formatting // (including years outside the [0,9999] interval). // // Subpackages provide: // // * `clock.Clock` which expresses a wall-clock style hours-minutes-seconds with millisecond precision. // // * `period.Period` which expresses a period corresponding to the ISO-8601 form (e.g. "PT30S"). // // * `timespan.DateRange` which expresses a period between two dates. // // * `timespan.TimeSpan` which expresses a duration of time between two instants. // // * `view.VDate` which wraps `Date` for use in templates etc. // // Credits // // This package follows very closely the design of package time // (http://golang.org/pkg/time/) in the standard library, many of the Date // methods are implemented using the corresponding methods of the time.Time // type, and much of the documentation is copied directly from that package. // // References // // https://golang.org/src/time/time.go // // https://en.wikipedia.org/wiki/Gregorian_calendar // // https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar // // https://en.wikipedia.org/wiki/Astronomical_year_numbering // // https://en.wikipedia.org/wiki/ISO_8601 // // https://tools.ietf.org/html/rfc822 // // https://tools.ietf.org/html/rfc850 // // https://tools.ietf.org/html/rfc1123 // // https://tools.ietf.org/html/rfc3339 // package date golang-github-rickb777-date-1.15.3/example_test.go000066400000000000000000000035061410771016400216260ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package date import ( "fmt" "time" ) func ExampleMax() { d := Max() fmt.Println(d) // Output: +5881580-07-11 } func ExampleMin() { d := Min() fmt.Println(d) // Output: -5877641-06-23 } func ExampleNew() { d := New(9999, time.December, 31) fmt.Printf("The world ends on %s\n", d) // Output: The world ends on 9999-12-31 } func ExampleParse() { // longForm shows by example how the reference date would be // represented in the desired layout. const longForm = "Mon, January 2, 2006" d, _ := Parse(longForm, "Tue, February 3, 2013") fmt.Println(d) // shortForm is another way the reference date would be represented // in the desired layout. const shortForm = "2006-Jan-02" d, _ = Parse(shortForm, "2013-Feb-03") fmt.Println(d) // Output: // 2013-02-03 // 2013-02-03 } func ExampleParseISO() { d, _ := ParseISO("+12345-06-07") year, month, day := d.Date() fmt.Println(year) fmt.Println(month) fmt.Println(day) // Output: // 12345 // June // 7 } func ExampleDate_AddDate() { d := New(1000, time.January, 1) // Months and days do not need to be constrained to [1,12] and [1,365]. u := d.AddDate(0, 14, -1) fmt.Println(u) // Output: 1001-02-28 } func ExampleDate_Format() { // layout shows by example how the reference time should be represented. const layout = "Jan 2, 2006" d := New(2009, time.November, 10) fmt.Println(d.Format(layout)) // Output: Nov 10, 2009 } func ExampleDate_FormatISO() { // According to legend, Rome was founded on April 21, 753 BC. // Note that with astronomical year numbering, 753 BC becomes -752 // because 1 BC is actually year 0. d := New(-752, time.April, 21) fmt.Println(d.FormatISO(5)) // Output: -00752-04-21 } golang-github-rickb777-date-1.15.3/format.go000066400000000000000000000121151410771016400204200ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package date import ( "fmt" "strings" ) // These are predefined layouts for use in Date.Format and Date.Parse. // The reference date used in the layouts is the same date used by the // time package in the standard library: // Monday, Jan 2, 2006 // To define your own format, write down what the reference date would look // like formatted your way; see the values of the predefined layouts for // examples. The model is to demonstrate what the reference date looks like // so that the Parse function and Format method can apply the same // transformation to a general date value. const ( ISO8601 = "2006-01-02" // ISO 8601 extended format ISO8601B = "20060102" // ISO 8601 basic format RFC822 = "02-Jan-06" RFC822W = "Mon, 02-Jan-06" // RFC822 with day of the week RFC850 = "Monday, 02-Jan-06" RFC1123 = "02 Jan 2006" RFC1123W = "Mon, 02 Jan 2006" // RFC1123 with day of the week RFC3339 = "2006-01-02" ) // String returns the time formatted in ISO 8601 extended format // (e.g. "2006-01-02"). If the year of the date falls outside the // [0,9999] range, this format produces an expanded year representation // with possibly extra year digits beyond the prescribed four-digit minimum // and with a + or - sign prefix (e.g. , "+12345-06-07", "-0987-06-05"). func (d Date) String() string { year, month, day := d.Date() if 0 <= year && year < 10000 { return fmt.Sprintf("%04d-%02d-%02d", year, month, day) } return fmt.Sprintf("%+05d-%02d-%02d", year, month, day) } // FormatISO returns a textual representation of the date value formatted // according to the expanded year variant of the ISO 8601 extended format; // the year of the date is represented as a signed integer using the // specified number of digits (ignored if less than four). // The string representation of the year will take more than the specified // number of digits if the magnitude of the year is too large to fit. // // Function Date.Format can be used to format Date values in other formats, // but it is currently not able to format dates according to the expanded // year variant of the ISO 8601 format. func (d Date) FormatISO(yearDigits int) string { n := 5 // four-digit minimum plus sign if yearDigits > 4 { n += yearDigits - 4 } year, month, day := d.Date() return fmt.Sprintf("%+0*d-%02d-%02d", n, year, month, day) } // Format returns a textual representation of the date value formatted according // to layout, which defines the format by showing how the reference date, // defined to be // Mon, Jan 2, 2006 // would be displayed if it were the value; it serves as an example of the // desired output. // // This function actually uses time.Format to format the input and can use any // layout accepted by time.Format by extending its date to a time at // 00:00:00.000 UTC. // // Additionally, it is able to insert the day-number suffix into the output string. // This is done by including "nd" in the format string, which will become // Mon, Jan 2nd, 2006 // For example, New Year's Day might be rendered as "Fri, Jan 1st, 2016". To alter // the suffix strings for a different locale, change DaySuffixes or use FormatWithSuffixes // instead. // // This function cannot currently format Date values according to the expanded // year variant of ISO 8601; you should use Date.FormatISO to that effect. func (d Date) Format(layout string) string { return d.FormatWithSuffixes(layout, DaySuffixes) } // FormatWithSuffixes is the same as Format, except the suffix strings can be specified // explicitly, which allows multiple locales to be supported. The suffixes slice should // contain 31 strings covering the days 1 (index 0) to 31 (index 30). func (d Date) FormatWithSuffixes(layout string, suffixes []string) string { t := decode(d.day) parts := strings.Split(layout, "nd") switch len(parts) { case 1: return t.Format(layout) default: // If the format contains "Monday", it has been split so repair it. i := 1 for i < len(parts) { if i > 0 && strings.HasSuffix(parts[i-1], "Mo") && strings.HasPrefix(parts[i], "ay") { parts[i-1] = parts[i-1] + "nd" + parts[i] copy(parts[i:], parts[i+1:]) parts = parts[:len(parts)-1] } else { i++ } } a := make([]string, 0, 2*len(parts)-1) for i, p := range parts { if i > 0 { a = append(a, suffixes[d.Day()-1]) } a = append(a, t.Format(p)) } return strings.Join(a, "") } } // DaySuffixes is the default array of strings used as suffixes when a format string // contains "nd" (as in "second"). This can be altered at startup in order to change // the default locale strings used for formatting dates. It supports every locale that // uses the Gregorian calendar and has a suffix after the day-of-month number. var DaySuffixes = []string{ "st", "nd", "rd", "th", "th", // 1 - 5 "th", "th", "th", "th", "th", // 6 - 10 "th", "th", "th", "th", "th", // 11 - 15 "th", "th", "th", "th", "th", // 16 - 20 "st", "nd", "rd", "th", "th", // 21 - 25 "th", "th", "th", "th", "th", // 26 - 30 "st", // 31 } golang-github-rickb777-date-1.15.3/format_test.go000066400000000000000000000042241410771016400214610ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package date import ( "testing" ) func TestString(t *testing.T) { cases := []struct { value string }{ {"-0001-01-01"}, {"0000-01-01"}, {"1000-01-01"}, {"1970-01-01"}, {"2000-11-22"}, {"+10000-01-01"}, } for _, c := range cases { d := MustParseISO(c.value) value := d.String() if value != c.value { t.Errorf("String() == %v, want %v", value, c.value) } } } func TestFormatISO(t *testing.T) { cases := []struct { value string n int }{ {"-5000-02-03", 4}, {"-05000-02-03", 5}, {"-005000-02-03", 6}, {"+0000-01-01", 4}, {"+00000-01-01", 5}, {"+1000-01-01", 4}, {"+01000-01-01", 5}, {"+1970-01-01", 4}, {"+001999-12-31", 6}, {"+999999-12-31", 6}, } for _, c := range cases { d := MustParseISO(c.value) value := d.FormatISO(c.n) if value != c.value { t.Errorf("FormatISO(%v) == %v, want %v", c, value, c.value) } } } func TestFormat(t *testing.T) { cases := []struct { value string format string expected string }{ {"1970-01-01", "2 Jan 2006", "1 Jan 1970"}, {"1970-01-01", "Jan 02 2006", "Jan 01 1970"}, {"1970-01-01", "Jan 2nd 2006", "Jan 1st 1970"}, {"2016-01-01", "2nd Jan 2006", "1st Jan 2016"}, {"2016-02-02", "Jan 2nd 2006", "Feb 2nd 2016"}, {"2016-03-03", "Jan 2nd 2006", "Mar 3rd 2016"}, {"2016-04-04", "2nd Jan 2006", "4th Apr 2016"}, {"2016-05-20", "Jan 2nd 2006", "May 20th 2016"}, {"2016-06-21", "Jan 2nd 2006", "Jun 21st 2016"}, {"2016-07-22", "Jan 2nd 2006", "Jul 22nd 2016"}, {"2016-08-23", "Jan 2nd 2006", "Aug 23rd 2016"}, {"2016-09-30", "Jan 2nd 2006", "Sep 30th 2016"}, {"2016-10-31", "Jan 2nd 2006", "Oct 31st 2016"}, {"2016-01-07", "Monday January 2nd 2006", "Thursday January 7th 2016"}, {"2016-01-07", "Monday 2nd Monday 2nd", "Thursday 7th Thursday 7th"}, {"2016-11-01", "2nd 2nd 2nd", "1st 1st 1st"}, } for _, c := range cases { d := MustParseISO(c.value) actual := d.Format(c.format) if actual != c.expected { t.Errorf("Format(%v) == %v, want %v", c, actual, c.expected) } } } golang-github-rickb777-date-1.15.3/go.mod000066400000000000000000000004761410771016400177160ustar00rootroot00000000000000module github.com/rickb777/date require ( github.com/onsi/gomega v1.10.4 github.com/rickb777/plural v1.3.0 golang.org/x/net v0.0.0-20201216054612-986b41b23924 // indirect golang.org/x/text v0.3.4 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) go 1.15 golang-github-rickb777-date-1.15.3/go.sum000066400000000000000000000156041410771016400177420ustar00rootroot00000000000000github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= github.com/rickb777/plural v1.2.2 h1:4CU5NiUqXSM++2+7JCrX+oguXd2D7RY5O1YisMw1yCI= github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= github.com/rickb777/plural v1.3.0 h1:cN3M4IcJCGiGpa92S3xJgiBQfqGDFj7J8JyObugVwAU= github.com/rickb777/plural v1.3.0/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= golang-github-rickb777-date-1.15.3/gregorian/000077500000000000000000000000001410771016400205565ustar00rootroot00000000000000golang-github-rickb777-date-1.15.3/gregorian/doc.go000066400000000000000000000015221410771016400216520ustar00rootroot00000000000000// Copyright 2016 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 gregorian provides utility functions for the Gregorian calendar calculations. // The Gregorian calendar was officially introduced on 15th October 1582 so, strictly speaking, // it only applies after that date. Some countries did not switch to the Gregorian calendar // for many years after (such as Great Britain in 1782). // // Extending the Gregorian calendar backwards to dates preceding its official introduction // produces a proleptic calendar that should be used with some caution for historic dates // because it can lead to confusion. // // See https://en.wikipedia.org/wiki/Gregorian_calendar // https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar package gregorian golang-github-rickb777-date-1.15.3/gregorian/util.go000066400000000000000000000014131410771016400220610ustar00rootroot00000000000000package gregorian import ( "time" ) // IsLeap simply tests whether a given year is a leap year, using the Gregorian calendar algorithm. func IsLeap(year int) bool { return year%4 == 0 && (year%100 != 0 || year%400 == 0) } // DaysInYear gives the number of days in a given year, according to the Gregorian calendar. func DaysInYear(year int) int { if IsLeap(year) { return 366 } return 365 } // DaysIn gives the number of days in a given month, according to the Gregorian calendar. func DaysIn(year int, month time.Month) int { if month == time.February && IsLeap(year) { return 29 } return daysInMonth[month] } var daysInMonth = []int{ 0, 31, // January 28, 31, // March 30, 31, // May 30, 31, // July 31, 30, // September 31, 30, // November 31, } golang-github-rickb777-date-1.15.3/gregorian/util_test.go000066400000000000000000000027351410771016400231300ustar00rootroot00000000000000package gregorian import ( "testing" "time" ) func TestIsLeap(t *testing.T) { cases := []struct { year int expected bool }{ {0, true}, // year zero is not defined under some conventions but is in ISO8601 {2000, true}, {2400, true}, {2001, false}, {2002, false}, {2003, false}, {2003, false}, {2004, true}, {2005, false}, {1800, false}, {1900, false}, {2200, false}, {2300, false}, {2500, false}, } for _, c := range cases { got := IsLeap(c.year) if got != c.expected { t.Errorf("TestIsLeap(%d) == %v, want %v", c.year, got, c.expected) } } } func TestDaysInYear(t *testing.T) { cases := []struct { year int expected int }{ {2000, 366}, {2001, 365}, } for _, c := range cases { got1 := DaysInYear(c.year) if got1 != c.expected { t.Errorf("DaysInYear(%d) == %v, want %v", c.year, got1, c.expected) } } } func TestDaysIn(t *testing.T) { cases := []struct { year int month time.Month expected int }{ {2000, time.January, 31}, {2000, time.February, 29}, {2001, time.February, 28}, {2001, time.April, 30}, {2001, time.May, 31}, {2001, time.June, 30}, {2001, time.July, 31}, {2001, time.August, 31}, {2001, time.September, 30}, {2001, time.October, 31}, {2001, time.November, 30}, {2001, time.December, 31}, } for _, c := range cases { got1 := DaysIn(c.year, c.month) if got1 != c.expected { t.Errorf("DaysIn(%d, %d) == %v, want %v", c.year, c.month, got1, c.expected) } } } golang-github-rickb777-date-1.15.3/marshal.go000066400000000000000000000056451410771016400205710ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package date import ( "errors" ) // MarshalBinary implements the encoding.BinaryMarshaler interface. func (d Date) MarshalBinary() ([]byte, error) { enc := []byte{ byte(d.day >> 24), byte(d.day >> 16), byte(d.day >> 8), byte(d.day), } return enc, nil } // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. func (d *Date) UnmarshalBinary(data []byte) error { if len(data) == 0 { return errors.New("Date.UnmarshalBinary: no data") } if len(data) != 4 { return errors.New("Date.UnmarshalBinary: invalid length") } d.day = PeriodOfDays(data[3]) | PeriodOfDays(data[2])<<8 | PeriodOfDays(data[1])<<16 | PeriodOfDays(data[0])<<24 return nil } // MarshalBinary implements the encoding.BinaryMarshaler interface. func (ds DateString) MarshalBinary() ([]byte, error) { return Date(ds).MarshalBinary() } // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. func (ds *DateString) UnmarshalBinary(data []byte) error { return (*Date)(ds).UnmarshalBinary(data) } // MarshalText implements the encoding.TextMarshaler interface. // The date is given in ISO 8601 extended format (e.g. "2006-01-02"). // If the year of the date falls outside the [0,9999] range, this format // produces an expanded year representation with possibly extra year digits // beyond the prescribed four-digit minimum and with a + or - sign prefix // (e.g. , "+12345-06-07", "-0987-06-05"). func (d Date) MarshalText() ([]byte, error) { return []byte(d.String()), nil } // UnmarshalText implements the encoding.TextUnmarshaler interface. // The date is expected to be in ISO 8601 extended format // (e.g. "2006-01-02", "+12345-06-07", "-0987-06-05"); // the year must use at least 4 digits and if outside the [0,9999] range // must be prefixed with a + or - sign. func (d *Date) UnmarshalText(data []byte) (err error) { u, err := ParseISO(string(data)) if err == nil { d.day = u.day } return err } // MarshalText implements the encoding.TextMarshaler interface. // The date is given in ISO 8601 extended format (e.g. "2006-01-02"). // If the year of the date falls outside the [0,9999] range, this format // produces an expanded year representation with possibly extra year digits // beyond the prescribed four-digit minimum and with a + or - sign prefix // (e.g. , "+12345-06-07", "-0987-06-05"). func (ds DateString) MarshalText() ([]byte, error) { return Date(ds).MarshalText() } // UnmarshalText implements the encoding.TextUnmarshaler interface. // The date is expected to be in ISO 8601 extended format // (e.g. "2006-01-02", "+12345-06-07", "-0987-06-05"); // the year must use at least 4 digits and if outside the [0,9999] range // must be prefixed with a + or - sign. func (ds *DateString) UnmarshalText(data []byte) (err error) { return (*Date)(ds).UnmarshalText(data) } golang-github-rickb777-date-1.15.3/marshal_test.go000066400000000000000000000132021410771016400216140ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package date import ( "bytes" "encoding/gob" "encoding/json" "testing" "time" ) func TestGobEncoding(t *testing.T) { cases := []Date{ New(-11111, time.February, 3), New(-1, time.December, 31), New(0, time.January, 1), New(1, time.January, 1), New(1970, time.January, 1), New(2012, time.June, 25), New(12345, time.June, 7), } for _, c := range cases { var b bytes.Buffer encoder := gob.NewEncoder(&b) decoder := gob.NewDecoder(&b) var d Date err := encoder.Encode(&c) if err != nil { t.Errorf("Gob(%v) encode error %v", c, err) } else { err = decoder.Decode(&d) if err != nil { t.Errorf("Gob(%v) decode error %v", c, err) } else if d != c { t.Errorf("Gob(%v) decode got %v", c, d) } } ds := c.DateString() err = encoder.Encode(&ds) if err != nil { t.Errorf("Gob(%v) encode error %v", c, err) } else { err = decoder.Decode(&ds) if err != nil { t.Errorf("Gob(%v) decode error %v", c, err) } else if ds != c.DateString() { t.Errorf("Gob(%v) decode got %v", c, ds) } } } } func TestDateJSONMarshalling(t *testing.T) { cases := []struct { value Date want string }{ {New(-11111, time.February, 3), `"-11111-02-03"`}, {New(-1, time.December, 31), `"-0001-12-31"`}, {New(0, time.January, 1), `"0000-01-01"`}, {New(1, time.January, 1), `"0001-01-01"`}, {New(1970, time.January, 1), `"1970-01-01"`}, {New(2012, time.June, 25), `"2012-06-25"`}, {New(12345, time.June, 7), `"+12345-06-07"`}, } for _, c := range cases { var d Date bb1, err := json.Marshal(c.value) if err != nil { t.Errorf("JSON(%v) marshal error %v", c, err) } else if string(bb1) != c.want { t.Errorf("JSON(%v) == %v, want %v", c.value, string(bb1), c.want) } else { err = json.Unmarshal(bb1, &d) if err != nil { t.Errorf("JSON(%v) unmarshal error %v", c.value, err) } else if d != c.value { t.Errorf("JSON(%v) unmarshal got %v", c.value, d) } } var ds DateString bb2, err := json.Marshal(c.value.DateString()) if err != nil { t.Errorf("JSON(%v) marshal error %v", c, err) } else if string(bb2) != c.want { t.Errorf("JSON(%v) == %v, want %v", c.value.DateString(), string(bb2), c.want) } else { err = json.Unmarshal(bb2, &ds) if err != nil { t.Errorf("JSON(%v) unmarshal error %v", c.value.DateString(), err) } else if ds != c.value.DateString() { t.Errorf("JSON(%v) unmarshal got %v", c.value.DateString(), ds) } } } } func TestDateTextMarshalling(t *testing.T) { cases := []struct { value Date want string }{ {New(-11111, time.February, 3), "-11111-02-03"}, {New(-1, time.December, 31), "-0001-12-31"}, {New(0, time.January, 1), "0000-01-01"}, {New(1, time.January, 1), "0001-01-01"}, {New(1970, time.January, 1), "1970-01-01"}, {New(2012, time.June, 25), "2012-06-25"}, {New(12345, time.June, 7), "+12345-06-07"}, } for _, c := range cases { var d Date bb1, err := c.value.MarshalText() if err != nil { t.Errorf("Text(%v) marshal error %v", c, err) } else if string(bb1) != c.want { t.Errorf("Text(%v) == %v, want %v", c.value, string(bb1), c.want) } else { err = d.UnmarshalText(bb1) if err != nil { t.Errorf("Text(%v) unmarshal error %v", c.value, err) } else if d != c.value { t.Errorf("Text(%v) unmarshal got %v", c.value, d) } } var ds DateString bb2, err := c.value.DateString().MarshalText() if err != nil { t.Errorf("Text(%v) marshal error %v", c, err) } else if string(bb2) != c.want { t.Errorf("Text(%v) == %v, want %v", c.value, string(bb2), c.want) } else { err = ds.UnmarshalText(bb2) if err != nil { t.Errorf("Text(%v) unmarshal error %v", c.value, err) } else if ds != c.value.DateString() { t.Errorf("Text(%v) unmarshal got %v", c.value, ds) } } } } func TestDateBinaryMarshalling(t *testing.T) { cases := []struct { value Date }{ {New(-11111, time.February, 3)}, {New(-1, time.December, 31)}, {New(0, time.January, 1)}, {New(1, time.January, 1)}, {New(1970, time.January, 1)}, {New(2012, time.June, 25)}, {New(12345, time.June, 7)}, } for _, c := range cases { bb1, err := c.value.MarshalBinary() if err != nil { t.Errorf("Binary(%v) marshal error %v", c, err) } else { var d Date err = d.UnmarshalBinary(bb1) if err != nil { t.Errorf("Binary(%v) unmarshal error %v", c.value, err) } else if d != c.value { t.Errorf("Binary(%v) unmarshal got %v", c.value, d) } } bb2, err := c.value.MarshalBinary() if err != nil { t.Errorf("Binary(%v) marshal error %v", c, err) } else { var ds DateString err = ds.UnmarshalBinary(bb2) if err != nil { t.Errorf("Binary(%v) unmarshal error %v", c.value, err) } else if ds != c.value.DateString() { t.Errorf("Binary(%v) unmarshal got %v", c.value, ds) } } } } func TestDateBinaryUnmarshallingErrors(t *testing.T) { var d Date err1 := d.UnmarshalBinary([]byte{}) if err1 == nil { t.Errorf("unmarshal no empty data error") } err2 := d.UnmarshalBinary([]byte("12345")) if err2 == nil { t.Errorf("unmarshal no wrong length error") } } func TestInvalidDateText(t *testing.T) { cases := []struct { value string want string }{ {`not-a-date`, `Date.ParseISO: cannot parse "not-a-date": incorrect syntax`}, {`215-08-15`, `Date.ParseISO: cannot parse "215-08-15": invalid year`}, } for _, c := range cases { var d Date err := d.UnmarshalText([]byte(c.value)) if err == nil || err.Error() != c.want { t.Errorf("InvalidText(%v) == %v, want %v", c.value, err, c.want) } } } golang-github-rickb777-date-1.15.3/parse.go000066400000000000000000000131541410771016400202460ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package date import ( "errors" "fmt" "strconv" "strings" "time" "unicode" ) // MustAutoParse is as per AutoParse except that it panics if the string cannot be parsed. // This is intended for setup code; don't use it for user inputs. func MustAutoParse(value string) Date { d, err := AutoParse(value) if err != nil { panic(err) } return d } // AutoParse is like ParseISO, except that it automatically adapts to a variety of date formats // provided that they can be detected unambiguously. Specifically, this includes the "European" // and "British" date formats but not the common US format. Surrounding whitespace is ignored. // The supported formats are: // // * all formats supported by ParseISO // // * yyyy/mm/dd | yyyy.mm.dd (or any similar pattern) // // * dd/mm/yyyy | dd.mm.yyyy (or any similar pattern) // // * surrounding whitespace is ignored // func AutoParse(value string) (Date, error) { abs := strings.TrimSpace(value) if len(abs) == 0 { return Date{}, errors.New("Date.AutoParse: cannot parse a blank string") } sign := "" if abs[0] == '+' || abs[0] == '-' { sign = abs[:1] abs = abs[1:] } if len(abs) >= 10 { i1 := -1 i2 := -1 for i, r := range abs { if unicode.IsPunct(r) { if i1 < 0 { i1 = i } else { i2 = i } } } if i1 >= 4 && i2 > i1 && abs[i1] == abs[i2] { // just normalise the punctuation a := []byte(abs) a[i1] = '-' a[i2] = '-' abs = string(a) } else if i1 >= 2 && i2 > i1 && abs[i1] == abs[i2] { // harder case - need to swap the field order dd := abs[0:i1] mm := abs[i1+1 : i2] yyyy := abs[i2+1:] abs = fmt.Sprintf("%s-%s-%s", yyyy, mm, dd) } } return ParseISO(sign + abs) } // MustParseISO is as per ParseISO except that it panics if the string cannot be parsed. // This is intended for setup code; don't use it for user inputs. func MustParseISO(value string) Date { d, err := ParseISO(value) if err != nil { panic(err) } return d } // ParseISO parses an ISO 8601 formatted string and returns the date value it represents. // In addition to the common formats (e.g. 2006-01-02 and 20060102), this function // accepts date strings using the expanded year representation // with possibly extra year digits beyond the prescribed four-digit minimum // and with a + or - sign prefix (e.g. , "+12345-06-07", "-0987-06-05"). // // Note that ParseISO is a little looser than the ISO 8601 standard and will // be happy to parse dates with a year longer in length than the four-digit minimum even // if they are missing the + sign prefix. // // Function date.Parse can be used to parse date strings in other formats, but it // is currently not able to parse ISO 8601 formatted strings that use the // expanded year format. // // Background: https://en.wikipedia.org/wiki/ISO_8601#Dates func ParseISO(value string) (Date, error) { if len(value) < 8 { return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %q: incorrect length", value) } abs := value if value[0] == '+' || value[0] == '-' { abs = value[1:] } dash1 := strings.IndexByte(abs, '-') fm1 := dash1 + 1 fm2 := dash1 + 3 fd1 := dash1 + 4 fd2 := dash1 + 6 if dash1 < 0 { // switch to YYYYMMDD format dash1 = 4 fm1 = 4 fm2 = 6 fd1 = 6 fd2 = 8 } else if abs[fm2] != '-' { return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %q: incorrect syntax", value) } //fmt.Printf("%s %d %d %d %d %d\n", value, dash1, fm1, fm2, fd1, fd2) if len(abs) != fd2 { return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %q: incorrect length", value) } year, err := parseField(value, abs[:dash1], "year", 4, -1) if err != nil { return Date{}, err } month, err := parseField(value, abs[fm1:fm2], "month", -1, 2) if err != nil { return Date{}, err } day, err := parseField(value, abs[fd1:], "day", -1, 2) if err != nil { return Date{}, err } if value[0] == '-' { year = -year } t := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) return Date{encode(t)}, nil } func parseField(value, field, name string, minLength, requiredLength int) (int, error) { if (minLength > 0 && len(field) < minLength) || (requiredLength > 0 && len(field) != requiredLength) { return 0, fmt.Errorf("Date.ParseISO: cannot parse %q: invalid %s", value, name) } number, err := strconv.Atoi(field) if err != nil { return 0, fmt.Errorf("Date.ParseISO: cannot parse %q: invalid %s", value, name) } return number, nil } // MustParse is as per 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 MustParse(layout, value string) Date { d, err := Parse(layout, value) if err != nil { panic(err) } return d } // Parse parses a formatted string of a known layout and returns the Date value it represents. // The layout defines the format by showing how the reference date, defined // to be // Monday, Jan 2, 2006 // would be interpreted if it were the value; it serves as an example of the // input format. The same interpretation will then be made to the input string. // // This function actually uses time.Parse to parse the input and can use any // layout accepted by time.Parse, but returns only the date part of the // parsed Time value. // // This function cannot currently parse ISO 8601 strings that use the expanded // year format; you should use date.ParseISO to parse those strings correctly. func Parse(layout, value string) (Date, error) { t, err := time.Parse(layout, value) if err != nil { return Date{}, err } return Date{encode(t)}, nil } golang-github-rickb777-date-1.15.3/parse_test.go000066400000000000000000000151271410771016400213070ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package date import ( "testing" "time" ) func TestAutoParse(t *testing.T) { cases := []struct { value string year int month time.Month day int }{ {" 31/12/1969 ", 1969, time.December, 31}, {"1969/12/31", 1969, time.December, 31}, {"1969.12.31", 1969, time.December, 31}, {"1969-12-31", 1969, time.December, 31}, {"+1970-01-01", 1970, time.January, 1}, {"+01970-01-02", 1970, time.January, 2}, {"2000-02-28", 2000, time.February, 28}, {"+2000-02-29", 2000, time.February, 29}, {"+02000-03-01", 2000, time.March, 1}, {"+002004-02-28", 2004, time.February, 28}, {"2004-02-29", 2004, time.February, 29}, {"2004-03-01", 2004, time.March, 1}, {"0000-01-01", 0, time.January, 1}, {"+0001-02-03", 1, time.February, 3}, {" +00019-03-04 ", 19, time.March, 4}, {"0100-04-05", 100, time.April, 5}, {"2000-05-06", 2000, time.May, 6}, {"+5000000-08-09", 5000000, time.August, 9}, {"-0001-09-11", -1, time.September, 11}, {" -0019-10-12 ", -19, time.October, 12}, {"-00100-11-13", -100, time.November, 13}, {"-02000-12-14", -2000, time.December, 14}, {"-30000-02-15", -30000, time.February, 15}, {"-0400000-05-16", -400000, time.May, 16}, {"-5000000-09-17", -5000000, time.September, 17}, {"12340506", 1234, time.May, 6}, {"+12340506", 1234, time.May, 6}, {"-00191012", -19, time.October, 12}, {" -00191012 ", -19, time.October, 12}, } for _, c := range cases { d := MustAutoParse(c.value) year, month, day := d.Date() if year != c.year || month != c.month || day != c.day { t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) } } badCases := []string{ "1234-05", "1234-5-6", "1234-05-6", "1234-5-06", "1234-0A-06", "1234-05-0B", "1234-05-06trailing", "padding1234-05-06", "1-02-03", "10-11-12", "100-02-03", "+1-02-03", "+10-11-12", "+100-02-03", "-123-05-06", "--", "", " ", } for _, c := range badCases { d, err := AutoParse(c) if err == nil { t.Errorf("ParseISO(%v) == %v", c, d) } } } func TestParseISO(t *testing.T) { cases := []struct { value string year int month time.Month day int }{ {"1969-12-31", 1969, time.December, 31}, {"+1970-01-01", 1970, time.January, 1}, {"+01970-01-02", 1970, time.January, 2}, {"2000-02-28", 2000, time.February, 28}, {"+2000-02-29", 2000, time.February, 29}, {"+02000-03-01", 2000, time.March, 1}, {"+002004-02-28", 2004, time.February, 28}, {"2004-02-29", 2004, time.February, 29}, {"2004-03-01", 2004, time.March, 1}, {"0000-01-01", 0, time.January, 1}, {"+0001-02-03", 1, time.February, 3}, {"+00019-03-04", 19, time.March, 4}, {"0100-04-05", 100, time.April, 5}, {"2000-05-06", 2000, time.May, 6}, {"+30000-06-07", 30000, time.June, 7}, {"+400000-07-08", 400000, time.July, 8}, {"+5000000-08-09", 5000000, time.August, 9}, {"-0001-09-11", -1, time.September, 11}, {"-0019-10-12", -19, time.October, 12}, {"-00100-11-13", -100, time.November, 13}, {"-02000-12-14", -2000, time.December, 14}, {"-30000-02-15", -30000, time.February, 15}, {"-0400000-05-16", -400000, time.May, 16}, {"-5000000-09-17", -5000000, time.September, 17}, {"12340506", 1234, time.May, 6}, {"+12340506", 1234, time.May, 6}, {"-00191012", -19, time.October, 12}, } for _, c := range cases { d := MustParseISO(c.value) year, month, day := d.Date() if year != c.year || month != c.month || day != c.day { t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) } } badCases := []string{ "1234-05", "1234-5-6", "1234-05-6", "1234-5-06", "1234/05/06", "1234-0A-06", "1234-05-0B", "1234-05-06trailing", "padding1234-05-06", "1-02-03", "10-11-12", "100-02-03", "+1-02-03", "+10-11-12", "+100-02-03", "-123-05-06", } for _, c := range badCases { d, err := ParseISO(c) if err == nil { t.Errorf("ParseISO(%v) == %v", c, d) } } } func BenchmarkParseISO(b *testing.B) { cases := []struct { layout string value string year int month time.Month day int }{ {ISO8601, "1969-12-31", 1969, time.December, 31}, {ISO8601, "2000-02-28", 2000, time.February, 28}, {ISO8601, "2004-02-29", 2004, time.February, 29}, {ISO8601, "2004-03-01", 2004, time.March, 1}, {ISO8601, "0000-01-01", 0, time.January, 1}, {ISO8601, "0001-02-03", 1, time.February, 3}, {ISO8601, "0100-04-05", 100, time.April, 5}, {ISO8601, "2000-05-06", 2000, time.May, 6}, } for n := 0; n < b.N; n++ { c := cases[n%len(cases)] _, err := ParseISO(c.value) if err != nil { b.Errorf("ParseISO(%v) == %v", c.value, err) } } } func TestParse(t *testing.T) { // Test ability to parse a few common date formats cases := []struct { layout string value string year int month time.Month day int }{ {ISO8601, "1969-12-31", 1969, time.December, 31}, {ISO8601B, "19700101", 1970, time.January, 1}, {RFC822, "29-Feb-00", 2000, time.February, 29}, {RFC822W, "Mon, 01-Mar-04", 2004, time.March, 1}, {RFC850, "Wednesday, 12-Aug-15", 2015, time.August, 12}, {RFC1123, "05 Dec 1928", 1928, time.December, 5}, {RFC1123W, "Mon, 05 Dec 1928", 1928, time.December, 5}, {RFC3339, "2345-06-07", 2345, time.June, 7}, {"20060102", "20190619", 2019, time.June, 19}, } for _, c := range cases { d := MustParse(c.layout, c.value) year, month, day := d.Date() if year != c.year || month != c.month || day != c.day { t.Errorf("Parse(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) } } // Test inability to parse ISO 8601 expanded year format badCases := []string{ "+1234-05-06", "+12345-06-07", "-1234-05-06", "-12345-06-07", } for _, c := range badCases { d, err := Parse(ISO8601, c) if err == nil { t.Errorf("Parse(%v) == %v", c, d) } } } func BenchmarkParse(b *testing.B) { // Test ability to parse a few common date formats cases := []struct { layout string value string year int month time.Month day int }{ {ISO8601, "1969-12-31", 1969, time.December, 31}, {ISO8601, "2000-02-28", 2000, time.February, 28}, {ISO8601, "2004-02-29", 2004, time.February, 29}, {ISO8601, "2004-03-01", 2004, time.March, 1}, {ISO8601, "0000-01-01", 0, time.January, 1}, {ISO8601, "0001-02-03", 1, time.February, 3}, {ISO8601, "0100-04-05", 100, time.April, 5}, {ISO8601, "2000-05-06", 2000, time.May, 6}, } for n := 0; n < b.N; n++ { c := cases[n%len(cases)] _, err := Parse(c.layout, c.value) if err != nil { b.Errorf("Parse(%v) == %v", c.value, err) } } } golang-github-rickb777-date-1.15.3/period/000077500000000000000000000000001410771016400200635ustar00rootroot00000000000000golang-github-rickb777-date-1.15.3/period/arithmetic.go000066400000000000000000000102441410771016400225440ustar00rootroot00000000000000// 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 period import ( "time" ) // Add adds two periods together. Use this method along with Negate in order to subtract periods. // // The result is not normalised and may overflow arithmetically (to make this unlikely, use Normalise on // the inputs before adding them). func (period Period) Add(that Period) Period { return Period{ period.years + that.years, period.months + that.months, period.days + that.days, period.hours + that.hours, period.minutes + that.minutes, period.seconds + that.seconds, } } //------------------------------------------------------------------------------------------------- // 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, 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 wholeDays := (period.days % 10) == 0 if wholeYears && wholeMonths && wholeDays { // in this case, time.AddDate provides an exact solution stE3 := totalSecondsE3(period) t1 := t.AddDate(int(period.years/10), int(period.months/10), int(period.days/10)) return t1.Add(stE3 * time.Millisecond), true } d, precise := period.Duration() return t.Add(d), precise } //------------------------------------------------------------------------------------------------- // Scale a period by a multiplication factor. Obviously, this can both enlarge and shrink it, // and change the sign if negative. The result is normalised, but integer overflows are silently // ignored. // // Bear in mind that the internal representation is limited by fixed-point arithmetic with two // decimal places; each field is only int16. // // Known issue: scaling by a large reduction factor (i.e. much less than one) doesn't work properly. func (period Period) Scale(factor float32) Period { result, _ := period.ScaleWithOverflowCheck(factor) return result } // ScaleWithOverflowCheck a period by a multiplication factor. Obviously, this can both enlarge and shrink it, // and change the sign if negative. The result is normalised. An error is returned if integer overflow // happened. // // Bear in mind that the internal representation is limited by fixed-point arithmetic with one // decimal place; each field is only int16. // // Known issue: scaling by a large reduction factor (i.e. much less than one) doesn't work properly. func (period Period) ScaleWithOverflowCheck(factor float32) (Period, error) { ap, neg := period.absNeg() if -0.5 < factor && factor < 0.5 { d, pr1 := ap.Duration() mul := float64(d) * float64(factor) p2, pr2 := NewOf(time.Duration(mul)) return p2.Normalise(pr1 && pr2), nil } y := int64(float32(ap.years) * factor) m := int64(float32(ap.months) * factor) d := int64(float32(ap.days) * factor) hh := int64(float32(ap.hours) * factor) mm := int64(float32(ap.minutes) * factor) ss := int64(float32(ap.seconds) * factor) p64 := &period64{years: y, months: m, days: d, hours: hh, minutes: mm, seconds: ss, neg: neg} return p64.normalise64(true).toPeriod() } // RationalScale scales a period by a rational multiplication factor. Obviously, this can both enlarge and shrink it, // and change the sign if negative. The result is normalised. An error is returned if integer overflow // happened. // // If the divisor is zero, a panic will arise. // // Bear in mind that the internal representation is limited by fixed-point arithmetic with two // decimal places; each field is only int16. //func (period Period) RationalScale(multiplier, divisor int) (Period, error) { // return period.rationalScale64(int64(multiplier), int64(divisor)) //} golang-github-rickb777-date-1.15.3/period/arithmetic_test.go000066400000000000000000000131331410771016400236030ustar00rootroot00000000000000// 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 period import ( "fmt" "testing" "time" . "github.com/onsi/gomega" ) func TestPeriodScale(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { one string m float32 expect string }{ {"P0D", 2, "P0D"}, {"P1D", 2, "P2D"}, {"P1D", 0, "P0D"}, {"P1D", 365, "P365D"}, {"P1M", 2, "P2M"}, {"P1M", 12, "P1Y"}, //TODO {"P1Y3M", 1.0/15, "P1M"}, {"P1Y", 2, "P2Y"}, {"PT1H", 2, "PT2H"}, {"PT1M", 2, "PT2M"}, {"PT1S", 2, "PT2S"}, {"P1D", 0.5, "P0.5D"}, {"P1M", 0.5, "P0.5M"}, {"P1Y", 0.5, "P0.5Y"}, {"PT1H", 0.5, "PT0.5H"}, {"PT1H", 0.1, "PT6M"}, //TODO {"PT1H", 0.01, "PT36S"}, {"PT1M", 0.5, "PT0.5M"}, {"PT1S", 0.5, "PT0.5S"}, {"PT1H", 1.0 / 3600, "PT1S"}, {"P1Y2M3DT4H5M6S", 2, "P2Y4M6DT8H10M12S"}, {"P2Y4M6DT8H10M12S", -0.5, "-P1Y2M3DT4H5M6S"}, {"-P2Y4M6DT8H10M12S", 0.5, "-P1Y2M3DT4H5M6S"}, {"-P2Y4M6DT8H10M12S", -0.5, "P1Y2M3DT4H5M6S"}, {"PT1M", 60, "PT1H"}, {"PT1S", 60, "PT1M"}, {"PT1S", 86400, "PT24H"}, {"PT1S", 86400000, "P1000D"}, {"P365.5D", 10, "P10Y2.5D"}, //{"P365.5D", 0.1, "P36DT12H"}, } for i, c := range cases { s := MustParse(c.one, false).Scale(c.m) g.Expect(s).To(Equal(MustParse(c.expect, false)), info(i, c.expect)) } } func TestPeriodAdd(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { one, two string expect string }{ {"P0D", "P0D", "P0D"}, {"P1D", "P1D", "P2D"}, {"P1M", "P1M", "P2M"}, {"P1Y", "P1Y", "P2Y"}, {"PT1H", "PT1H", "PT2H"}, {"PT1M", "PT1M", "PT2M"}, {"PT1S", "PT1S", "PT2S"}, {"P1Y2M3DT4H5M6S", "P6Y5M4DT3H2M1S", "P7Y7M7DT7H7M7S"}, {"P7Y7M7DT7H7M7S", "-P7Y7M7DT7H7M7S", "P0D"}, } for i, c := range cases { s := MustParse(c.one, false).Add(MustParse(c.two, false)) expectValid(t, s, info(i, c.expect)) g.Expect(s).To(Equal(MustParse(c.expect, false)), info(i, c.expect)) } } func TestPeriodAddToTime(t *testing.T) { g := NewGomegaWithT(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).UTC() cases := []struct { value string result time.Time precise bool }{ // precise cases {"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.AddDate(0, 0, 1), true}, {"P3276D", t0.AddDate(0, 0, 3276), true}, {"P1M", t0.AddDate(0, 1, 0), true}, {"P3276M", t0.AddDate(0, 3276, 0), true}, {"P1Y", t0.AddDate(1, 0, 0), true}, {"-P1Y", t0.AddDate(-1, 0, 0), true}, {"P3276Y", t0.AddDate(3276, 0, 0), true}, // near the upper limit of range {"-P3276Y", t0.AddDate(-3276, 0, 0), true}, // near the lower limit of range // approximate cases {"P0.1D", t0.Add(144 * min), false}, {"-P0.1D", t0.Add(-144 * min), false}, {"P0.1M", t0.Add(oneMonthApprox / 10), false}, {"P0.1Y", t0.Add(oneYearApprox / 10), false}, } for i, c := range cases { p := MustParse(c.value) t1, prec := p.AddTo(t0) g.Expect(t1).To(Equal(c.result), info(i, c.value)) g.Expect(prec).To(Equal(c.precise), info(i, c.value)) } } func expectValid(t *testing.T, period Period, hint interface{}) Period { t.Helper() g := NewGomegaWithT(t) info := fmt.Sprintf("%v: invalid: %#v", hint, period) // check all the signs are consistent nPoz := pos(period.years) + pos(period.months) + pos(period.days) + pos(period.hours) + pos(period.minutes) + pos(period.seconds) nNeg := neg(period.years) + neg(period.months) + neg(period.days) + neg(period.hours) + neg(period.minutes) + neg(period.seconds) g.Expect(nPoz == 0 || nNeg == 0).To(BeTrue(), info+" inconsistent signs") // only one field must have a fraction yearsFraction := fraction(period.years) monthsFraction := fraction(period.months) daysFraction := fraction(period.days) hoursFraction := fraction(period.hours) minutesFraction := fraction(period.minutes) if yearsFraction != 0 { g.Expect(period.months).To(BeZero(), info+" year fraction exists") g.Expect(period.days).To(BeZero(), info+" year fraction exists") g.Expect(period.hours).To(BeZero(), info+" year fraction exists") g.Expect(period.minutes).To(BeZero(), info+" year fraction exists") g.Expect(period.seconds).To(BeZero(), info+" year fraction exists") } if monthsFraction != 0 { g.Expect(period.days).To(BeZero(), info+" month fraction exists") g.Expect(period.hours).To(BeZero(), info+" month fraction exists") g.Expect(period.minutes).To(BeZero(), info+" month fraction exists") g.Expect(period.seconds).To(BeZero(), info+" month fraction exists") } if daysFraction != 0 { g.Expect(period.hours).To(BeZero(), info+" day fraction exists") g.Expect(period.minutes).To(BeZero(), info+" day fraction exists") g.Expect(period.seconds).To(BeZero(), info+" day fraction exists") } if hoursFraction != 0 { g.Expect(period.minutes).To(BeZero(), info+" hour fraction exists") g.Expect(period.seconds).To(BeZero(), info+" hour fraction exists") } if minutesFraction != 0 { g.Expect(period.seconds).To(BeZero(), info+" minute fraction exists") } return period } func fraction(i int16) int { return int(i) % 10 } func pos(i int16) int { if i > 0 { return 1 } return 0 } func neg(i int16) int { if i < 0 { return 1 } return 0 } golang-github-rickb777-date-1.15.3/period/designator.go000066400000000000000000000017161410771016400225560ustar00rootroot00000000000000// 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 period type ymdDesignator byte type hmsDesignator byte const ( Year ymdDesignator = 'Y' Month ymdDesignator = 'M' Week ymdDesignator = 'W' Day ymdDesignator = 'D' Hour hmsDesignator = 'H' Minute hmsDesignator = 'M' Second hmsDesignator = 'S' ) func (d ymdDesignator) IsOneOf(xx ...ymdDesignator) bool { for _, x := range xx { if x == d { return true } } return false } func (d ymdDesignator) IsNotOneOf(xx ...ymdDesignator) bool { for _, x := range xx { if x == d { return false } } return true } func (d hmsDesignator) IsOneOf(xx ...hmsDesignator) bool { for _, x := range xx { if x == d { return true } } return false } func (d hmsDesignator) IsNotOneOf(xx ...hmsDesignator) bool { for _, x := range xx { if x == d { return false } } return true } golang-github-rickb777-date-1.15.3/period/doc.go000066400000000000000000000024001410771016400211530ustar00rootroot00000000000000// Copyright 2016 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 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. // // See https://en.wikipedia.org/wiki/ISO_8601#Durations // // Example representations: // // * "P2Y" is two years; // // * "P6M" is six months; // // * "P4D" is four days; // // * "P1W" is one week (seven days); // // * "PT3H" is three hours. // // * "PT20M" is twenty minutes. // // * "PT30S" is thirty seconds. // // These can be combined, for example: // // * "P3Y6M4W1D" is three years, 6 months, 4 weeks and one day. // // * "P2DT12H" is 2 days and 12 hours. // // Also, decimal fractions are supported to one decimal place. To comply with // the standard, only the last non-zero component is allowed to have a fraction. // For example // // * "P2.5Y" is 2.5 years. // // * "PT12M7.5S" is 12 minutes and 7.5 seconds. // package period golang-github-rickb777-date-1.15.3/period/flag.go000066400000000000000000000004601410771016400213230ustar00rootroot00000000000000package period // Set enables use of Period by the flag API. func (period *Period) Set(p string) error { p2, err := Parse(p) if err != nil { return err } *period = p2 return nil } // Type is for compatibility with the spf13/pflag library. func (period Period) Type() string { return "period" } golang-github-rickb777-date-1.15.3/period/format.go000066400000000000000000000105061410771016400217040ustar00rootroot00000000000000// 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 period import ( "fmt" "io" "strings" "github.com/rickb777/plural" ) // Format converts the period to human-readable form using the default localisation. // Multiples of 7 days are shown as weeks. func (period Period) Format() string { return period.FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, PeriodWeekNames, PeriodDayNames, PeriodHourNames, PeriodMinuteNames, PeriodSecondNames) } // FormatWithoutWeeks converts the period to human-readable form using the default localisation. // Multiples of 7 days are not shown as weeks. func (period Period) FormatWithoutWeeks() string { return period.FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, plural.Plurals{}, PeriodDayNames, PeriodHourNames, PeriodMinuteNames, PeriodSecondNames) } // FormatWithPeriodNames converts the period to human-readable form in a localisable way. func (period Period) FormatWithPeriodNames(yearNames, monthNames, weekNames, dayNames, hourNames, minNames, secNames plural.Plurals) string { period = period.Abs() parts := make([]string, 0) parts = appendNonBlank(parts, yearNames.FormatFloat(float10(period.years))) parts = appendNonBlank(parts, monthNames.FormatFloat(float10(period.months))) if period.days > 0 || (period.IsZero()) { if len(weekNames) > 0 { weeks := period.days / 70 mdays := period.days % 70 //fmt.Printf("%v %#v - %d %d\n", period, period, weeks, mdays) if weeks > 0 { parts = appendNonBlank(parts, weekNames.FormatInt(int(weeks))) } if mdays > 0 || weeks == 0 { parts = appendNonBlank(parts, dayNames.FormatFloat(float10(mdays))) } } else { parts = appendNonBlank(parts, dayNames.FormatFloat(float10(period.days))) } } parts = appendNonBlank(parts, hourNames.FormatFloat(float10(period.hours))) parts = appendNonBlank(parts, minNames.FormatFloat(float10(period.minutes))) parts = appendNonBlank(parts, secNames.FormatFloat(float10(period.seconds))) return strings.Join(parts, ", ") } func appendNonBlank(parts []string, s string) []string { if s == "" { return parts } return append(parts, s) } // PeriodDayNames provides the English default format names for the days part of the period. // This is a sequence of plurals where the first match is used, otherwise the last one is used. // The last one must include a "%v" placeholder for the number. var PeriodDayNames = plural.FromZero("%v days", "%v day", "%v days") // PeriodWeekNames is as for PeriodDayNames but for weeks. var PeriodWeekNames = plural.FromZero("", "%v week", "%v weeks") // PeriodMonthNames is as for PeriodDayNames but for months. var PeriodMonthNames = plural.FromZero("", "%v month", "%v months") // PeriodYearNames is as for PeriodDayNames but for years. var PeriodYearNames = plural.FromZero("", "%v year", "%v years") // PeriodHourNames is as for PeriodDayNames but for hours. var PeriodHourNames = plural.FromZero("", "%v hour", "%v hours") // PeriodMinuteNames is as for PeriodDayNames but for minutes. var PeriodMinuteNames = plural.FromZero("", "%v minute", "%v minutes") // PeriodSecondNames is as for PeriodDayNames but for seconds. var PeriodSecondNames = plural.FromZero("", "%v second", "%v seconds") // String converts the period to ISO-8601 form. func (period Period) String() string { return period.toPeriod64("").String() } func (p64 period64) String() string { if p64 == (period64{}) { return "P0D" } buf := &strings.Builder{} if p64.neg { buf.WriteByte('-') } buf.WriteByte('P') writeField64(buf, p64.years, byte(Year)) writeField64(buf, p64.months, byte(Month)) if p64.days != 0 { if p64.days%70 == 0 { writeField64(buf, p64.days/7, byte(Week)) } else { writeField64(buf, p64.days, byte(Day)) } } if p64.hours != 0 || p64.minutes != 0 || p64.seconds != 0 { buf.WriteByte('T') } writeField64(buf, p64.hours, byte(Hour)) writeField64(buf, p64.minutes, byte(Minute)) writeField64(buf, p64.seconds, byte(Second)) return buf.String() } func writeField64(w io.Writer, field int64, designator byte) { if field != 0 { if field%10 != 0 { fmt.Fprintf(w, "%g", float32(field)/10) } else { fmt.Fprintf(w, "%d", field/10) } w.(io.ByteWriter).WriteByte(designator) } } func float10(v int16) float32 { return float32(v) / 10 } golang-github-rickb777-date-1.15.3/period/marshal.go000066400000000000000000000022211410771016400220360ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package period // MarshalBinary implements the encoding.BinaryMarshaler interface. // This also provides support for gob encoding. func (period Period) MarshalBinary() ([]byte, error) { // binary method would take more space in many cases, so we simply use text return period.MarshalText() } // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. // This also provides support for gob decoding. func (period *Period) UnmarshalBinary(data []byte) error { return period.UnmarshalText(data) } // MarshalText implements the encoding.TextMarshaler interface for Periods. // This also provides support for JSON encoding. func (period Period) MarshalText() ([]byte, error) { return []byte(period.String()), nil } // UnmarshalText implements the encoding.TextUnmarshaler interface for Periods. // This also provides support for JSON decoding. func (period *Period) UnmarshalText(data []byte) (err error) { u, err := Parse(string(data), false) if err == nil { *period = u } return err } golang-github-rickb777-date-1.15.3/period/marshal_test.go000066400000000000000000000060251410771016400231030ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package period import ( "bytes" "encoding/gob" "encoding/json" "testing" . "github.com/onsi/gomega" ) func TestGobEncoding(t *testing.T) { g := NewGomegaWithT(t) var b bytes.Buffer encoder := gob.NewEncoder(&b) decoder := gob.NewDecoder(&b) cases := []string{ "P0D", "P1D", "P1W", "P1M", "P1Y", "PT1H", "PT1M", "PT1S", "P2Y3M4W5D", "-P2Y3M4W5D", "P2Y3M4W5DT1H7M9S", "-P2Y3M4W5DT1H7M9S", "P48M", } for i, c := range cases { period := MustParse(c, false) var p Period err := encoder.Encode(&period) g.Expect(err).NotTo(HaveOccurred(), info(i, c)) if err == nil { err = decoder.Decode(&p) g.Expect(err).NotTo(HaveOccurred(), info(i, c)) g.Expect(p).To(Equal(period), info(i, c)) } } } func TestPeriodJSONMarshalling(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value Period want string }{ {New(-1111, -4, -3, -11, -59, -59), `"-P1111Y4M3DT11H59M59S"`}, {New(-1, -10, -31, -5, -4, -20), `"-P1Y10M31DT5H4M20S"`}, {New(0, 0, 0, 0, 0, 0), `"P0D"`}, {New(0, 0, 0, 0, 0, 1), `"PT1S"`}, {New(0, 0, 0, 0, 1, 0), `"PT1M"`}, {New(0, 0, 0, 1, 0, 0), `"PT1H"`}, {New(0, 0, 1, 0, 0, 0), `"P1D"`}, {New(0, 1, 0, 0, 0, 0), `"P1M"`}, {New(1, 0, 0, 0, 0, 0), `"P1Y"`}, } for i, c := range cases { var p Period bb, err := json.Marshal(c.value) g.Expect(err).NotTo(HaveOccurred(), info(i, c)) g.Expect(string(bb)).To(Equal(c.want), info(i, c)) if string(bb) == c.want { err = json.Unmarshal(bb, &p) g.Expect(err).NotTo(HaveOccurred(), info(i, c)) g.Expect(p).To(Equal(c.value), info(i, c)) } } } func TestPeriodTextMarshalling(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value Period want string }{ {New(-1111, -4, -3, -11, -59, -59), "-P1111Y4M3DT11H59M59S"}, {New(-1, -9, -31, -5, -4, -20), "-P1Y9M31DT5H4M20S"}, {New(0, 0, 0, 0, 0, 0), "P0D"}, {New(0, 0, 0, 0, 0, 1), "PT1S"}, {New(0, 0, 0, 0, 1, 0), "PT1M"}, {New(0, 0, 0, 1, 0, 0), "PT1H"}, {New(0, 0, 1, 0, 0, 0), "P1D"}, {New(0, 1, 0, 0, 0, 0), "P1M"}, {New(1, 0, 0, 0, 0, 0), "P1Y"}, } for i, c := range cases { var p Period bb, err := c.value.MarshalText() g.Expect(err).NotTo(HaveOccurred(), info(i, c)) g.Expect(string(bb)).To(Equal(c.want), info(i, c)) if string(bb) == c.want { err = p.UnmarshalText(bb) g.Expect(err).NotTo(HaveOccurred(), info(i, c)) g.Expect(p).To(Equal(c.value), info(i, c)) } } } func TestInvalidPeriodText(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value string want string }{ {``, `cannot parse a blank string as a period`}, {`not-a-period`, `not-a-period: expected 'P' period mark at the start`}, {`P000`, `P000: missing designator at the end`}, } for i, c := range cases { var p Period err := p.UnmarshalText([]byte(c.value)) g.Expect(err).To(HaveOccurred(), info(i, c)) g.Expect(err.Error()).To(Equal(c.want), info(i, c)) } } golang-github-rickb777-date-1.15.3/period/parse.go000066400000000000000000000146431410771016400215340ustar00rootroot00000000000000// 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 period import ( "fmt" "strconv" "strings" ) // MustParse is as per Parse except that it panics if the string cannot be parsed. // This is intended for setup code; don't use it for user inputs. // By default, the value is normalised. // Normalisation can be disabled using the optional flag. func MustParse(value string, normalise ...bool) Period { d, err := Parse(value, normalise...) if err != nil { panic(err) } return d } // Parse parses strings that specify periods using ISO-8601 rules. // // In addition, a plus or minus sign can precede the period, e.g. "-P10D" // // By default, the value is normalised, e.g. multiple of 12 months become years // so "P24M" is the same as "P2Y". However, this is done without loss of precision, // so for example whole numbers of days do not contribute to the months tally // because the number of days per month is variable. // // Normalisation can be disabled using the optional flag. // // 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 Parse(period string, normalise ...bool) (Period, error) { return ParseWithNormalise(period, len(normalise) == 0 || normalise[0]) } // ParseWithNormalise parses strings that specify periods using ISO-8601 rules // with an option to specify whether to normalise parsed period components. // // This method is deprecated and should not be used. It may be removed in a // future version. func ParseWithNormalise(period string, normalise bool) (Period, error) { if period == "" || period == "-" || period == "+" { return Period{}, fmt.Errorf("cannot parse a blank string as a period") } if period == "P0" { return Period{}, nil } p64, err := parse(period, normalise) if err != nil { return Period{}, err } return p64.toPeriod() } func parse(period string, normalise bool) (*period64, error) { neg := false remaining := period if remaining[0] == '-' { neg = true remaining = remaining[1:] } else if remaining[0] == '+' { remaining = remaining[1:] } if remaining[0] != 'P' { return nil, fmt.Errorf("%s: expected 'P' period mark at the start", period) } remaining = remaining[1:] var number, weekValue, prevFraction int64 result := &period64{input: period, neg: neg} var years, months, weeks, days, hours, minutes, seconds itemState var designator, prevDesignator byte var err error nComponents := 0 years, months, weeks, days = Armed, Armed, Armed, Armed isHMS := false for len(remaining) > 0 { if remaining[0] == 'T' { if isHMS { return nil, fmt.Errorf("%s: 'T' designator cannot occur more than once", period) } isHMS = true years, months, weeks, days = Unready, Unready, Unready, Unready hours, minutes, seconds = Armed, Armed, Armed remaining = remaining[1:] } else { number, designator, remaining, err = parseNextField(remaining, period) if err != nil { return nil, err } fraction := number % 10 if prevFraction != 0 && fraction != 0 { return nil, fmt.Errorf("%s: '%c' & '%c' only the last field can have a fraction", period, prevDesignator, designator) } switch designator { case 'Y': years, err = years.testAndSet(number, 'Y', result, &result.years) case 'W': weeks, err = weeks.testAndSet(number, 'W', result, &weekValue) case 'D': days, err = days.testAndSet(number, 'D', result, &result.days) case 'H': hours, err = hours.testAndSet(number, 'H', result, &result.hours) case 'S': seconds, err = seconds.testAndSet(number, 'S', result, &result.seconds) case 'M': if isHMS { minutes, err = minutes.testAndSet(number, 'M', result, &result.minutes) } else { months, err = months.testAndSet(number, 'M', result, &result.months) } default: return nil, fmt.Errorf("%s: expected a number not '%c'", period, designator) } nComponents++ if err != nil { return nil, err } prevFraction = fraction prevDesignator = designator } } if nComponents == 0 { return nil, fmt.Errorf("%s: expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' designator", period) } result.days += weekValue * 7 if normalise { result = result.normalise64(true) } return result, nil } //------------------------------------------------------------------------------------------------- type itemState int const ( Unready itemState = iota Armed Set ) func (i itemState) testAndSet(number int64, designator byte, result *period64, value *int64) (itemState, error) { switch i { case Unready: return i, fmt.Errorf("%s: '%c' designator cannot occur here", result.input, designator) case Set: return i, fmt.Errorf("%s: '%c' designator cannot occur more than once", result.input, designator) } *value = number return Set, nil } //------------------------------------------------------------------------------------------------- func parseNextField(str, original string) (int64, byte, string, error) { i := scanDigits(str) if i < 0 { return 0, 0, "", fmt.Errorf("%s: missing designator at the end", original) } des := str[i] number, err := parseDecimalNumber(str[:i], original, des) return number, des, str[i+1:], err } // Fixed-point one decimal place func parseDecimalNumber(number, original string, des byte) (int64, error) { dec := strings.IndexByte(number, '.') if dec < 0 { dec = strings.IndexByte(number, ',') } var integer, fraction int64 var err error if dec >= 0 { integer, err = strconv.ParseInt(number[:dec], 10, 64) if err == nil { number = number[dec+1:] switch len(number) { case 0: // skip case 1: fraction, err = strconv.ParseInt(number, 10, 64) default: fraction, err = strconv.ParseInt(number[:1], 10, 64) } } } else { integer, err = strconv.ParseInt(number, 10, 64) } if err != nil { return 0, fmt.Errorf("%s: expected a number but found '%c'", original, des) } return integer*10 + fraction, err } // scanDigits finds the first non-digit byte after a given starting point. // Note that it does not care about runes or UTF-8 encoding; it assumes that // a period string is always valid ASCII as well as UTF-8. func scanDigits(s string) int { for i, c := range s { if !isDigit(c) { return i } } return -1 } func isDigit(c rune) bool { return ('0' <= c && c <= '9') || c == '.' || c == ',' } golang-github-rickb777-date-1.15.3/period/period.go000066400000000000000000000543171410771016400217060ustar00rootroot00000000000000// 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 period import ( "fmt" "time" ) const daysPerYearE4 = 3652425 // 365.2425 days by the Gregorian rule const daysPerMonthE4 = 304369 // 30.4369 days per month const daysPerMonthE6 = 30436875 // 30.436875 days per month const oneE4 = 10000 const oneE5 = 100000 const oneE6 = 1000000 const oneE7 = 10000000 const hundredMs = 100 * time.Millisecond // reminder: int64 overflow is after 9,223,372,036,854,775,807 (math.MaxInt64) // Period holds a period of time and provides conversion to/from ISO-8601 representations. // Therefore there are six fields: years, months, 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. // // However, in this implementation, the precision is limited to one decimal place only, by // means of integers with fixed point arithmetic. (This avoids using float32 in the struct, // so there are no problems testing equality using ==.) // // The implementation limits the range of possible values to ± 2^16 / 10 in each field. // Note in particular that the range of years is limited to approximately ± 3276. // // The concept of weeks exists in string representations of periods, but otherwise weeks // are unimportant. The period contains a number of days from which the number of weeks can // be calculated when needed. // // Note that although fractional weeks can be parsed, they will never be returned via String(). // This is because the number of weeks is always inferred from the number of days. // type Period struct { years, months, days, hours, minutes, seconds int16 } // 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 the Normalise method if you // need to. // // All the parameters must have the same sign (otherwise a panic occurs). // Because this implementation uses int16 internally, the paramters must // be within the range ± 2^16 / 10. func NewYMD(years, months, days int) Period { return New(years, months, days, 0, 0, 0) } // 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 the Normalise method // if you need to. // // All the parameters must have the same sign (otherwise a panic occurs). // Because this implementation uses int16 internally, the paramters must // be within the range ± 2^16 / 10. func NewHMS(hours, minutes, seconds int) Period { return New(0, 0, 0, hours, minutes, seconds) } // 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 the Normalise method // if you need to. // // All the parameters must have the same sign (otherwise a panic occurs). func New(years, months, days, hours, minutes, seconds int) Period { if (years >= 0 && months >= 0 && days >= 0 && hours >= 0 && minutes >= 0 && seconds >= 0) || (years <= 0 && months <= 0 && days <= 0 && hours <= 0 && minutes <= 0 && seconds <= 0) { return Period{ int16(years) * 10, int16(months) * 10, int16(days) * 10, int16(hours) * 10, int16(minutes) * 10, int16(seconds) * 10, } } panic(fmt.Sprintf("Periods must have homogeneous signs; got P%dY%dM%dDT%dH%dM%dS", years, months, days, hours, minutes, seconds)) } // 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, 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 if duration < 0 { sign = -1 d = -duration } sign10 := sign * 10 totalHours := int64(d / time.Hour) // check for 16-bit overflow - occurs near the 4.5 month mark if totalHours < 3277 { // simple HMS case minutes := d % time.Hour / time.Minute seconds := d % time.Minute / hundredMs return Period{0, 0, 0, sign10 * int16(totalHours), sign10 * int16(minutes), sign * int16(seconds)}, true } totalDays := totalHours / 24 // ignoring daylight savings adjustments if totalDays < 3277 { hours := totalHours - totalDays*24 minutes := d % time.Hour / time.Minute seconds := d % time.Minute / hundredMs return Period{0, 0, sign10 * int16(totalDays), sign10 * int16(hours), sign10 * int16(minutes), sign * int16(seconds)}, false } // TODO it is uncertain whether this is too imprecise and should be improved years := (oneE4 * totalDays) / daysPerYearE4 months := ((oneE4 * totalDays) / daysPerMonthE4) - (12 * years) hours := totalHours - totalDays*24 totalDays = ((totalDays * oneE4) - (daysPerMonthE4 * months) - (daysPerYearE4 * years)) / oneE4 return Period{sign10 * int16(years), sign10 * int16(months), sign10 * int16(totalDays), sign10 * int16(hours), 0, 0}, false } // Between converts the span between two times to a period. Based on the Gregorian conversion // algorithms of `time.Time`, the resultant period is precise. // // 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, // day) part, or the clock (hour, minute, second) part, but not both. func Between(t1, t2 time.Time) (p Period) { sign := 1 if t2.Before(t1) { t1, t2, sign = t2, t1, -1 } if t1.Location() != t2.Location() { t2 = t2.In(t1.Location()) } year, month, day, hour, min, sec, hundredth := daysDiff(t1, t2) if sign < 0 { p = New(-year, -month, -day, -hour, -min, -sec) p.seconds -= int16(hundredth) } else { p = New(year, month, day, hour, min, sec) p.seconds += int16(hundredth) } return } func daysDiff(t1, t2 time.Time) (year, month, day, hour, min, sec, hundredth int) { duration := t2.Sub(t1) hh1, mm1, ss1 := t1.Clock() hh2, mm2, ss2 := t2.Clock() day = int(duration / (24 * time.Hour)) hour = hh2 - hh1 min = mm2 - mm1 sec = ss2 - ss1 hundredth = (t2.Nanosecond() - t1.Nanosecond()) / 100000000 // Normalize negative values if sec < 0 { sec += 60 min-- } if min < 0 { min += 60 hour-- } if hour < 0 { hour += 24 // no need to reduce day - it's calculated differently. } // test 16bit storage limit (with 1 fixed decimal place) if day > 3276 { y1, m1, d1 := t1.Date() y2, m2, d2 := t2.Date() year = y2 - y1 month = int(m2 - m1) day = d2 - d1 } return } // IsZero returns true if applied to a zero-length period. 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 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 { return Period{period.years, period.months, period.days, 0, 0, 0} } // OnlyHMS returns a new Period with only the hour, minute and second fields. The year, // month and day fields are zeroed. func (period Period) OnlyHMS() Period { return Period{0, 0, 0, period.hours, period.minutes, period.seconds} } // Abs converts a negative period to a positive one. func (period Period) Abs() Period { a, _ := period.absNeg() return a } func (period Period) absNeg() (Period, bool) { if period.IsNegative() { return period.Negate(), true } return period, false } func (period Period) condNegate(neg bool) Period { if neg { return period.Negate() } return period } // Negate changes the sign of the period. func (period Period) Negate() Period { return Period{-period.years, -period.months, -period.days, -period.hours, -period.minutes, -period.seconds} } func absInt16(v int16) int16 { if v < 0 { return -v } return v } // 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 { return int(period.YearsFloat()) } // YearsFloat gets the number of years in the period, including a fraction if any is present. // The result is the number of years and does not include any other field. func (period Period) YearsFloat() float32 { return float32(period.years) / 10 } // Months gets the whole number of months in the period. // The result is the number of months and does not include any other field. // // Note that after normalisation, whole multiple of 12 months are added to // the number of years, so the number of months will be reduced correspondingly. func (period Period) Months() int { return int(period.MonthsFloat()) } // MonthsFloat gets the number of months in the period. // The result is the number of months and does not include any other field. // // Note that after normalisation, whole multiple of 12 months are added to // the number of years, so the number of months will be reduced correspondingly. func (period Period) MonthsFloat() float32 { return float32(period.months) / 10 } // Days gets the whole number of days in the period. This includes the implied // number of weeks but does not include any other field. func (period Period) Days() int { return int(period.DaysFloat()) } // DaysFloat gets the number of days in the period. This includes the implied // number of weeks but does not include any other field. func (period Period) DaysFloat() float32 { return float32(period.days) / 10 } // Weeks calculates the number of whole weeks from the number of days. If the result // would contain a fraction, it is truncated. // The result is the number of weeks and does not include any other field. // // Note that weeks are synthetic: they are internally represented using days. // See ModuloDays(), which returns the number of days excluding whole weeks. func (period Period) Weeks() int { return int(period.days) / 70 } // WeeksFloat calculates the number of weeks from the number of days. // The result is the number of weeks and does not include any other field. func (period Period) WeeksFloat() float32 { return float32(period.days) / 70 } // ModuloDays calculates the whole number of days remaining after the whole number of weeks // has been excluded. func (period Period) ModuloDays() int { days := absInt16(period.days) % 70 f := int(days / 10) if period.days < 0 { return -f } return f } // Hours gets the whole number of hours in the period. // The result is the number of hours and does not include any other field. func (period Period) Hours() int { return int(period.HoursFloat()) } // HoursFloat gets the number of hours in the period. // The result is the number of hours and does not include any other field. func (period Period) HoursFloat() float32 { return float32(period.hours) / 10 } // Minutes gets the whole number of minutes in the period. // The result is the number of minutes and does not include any other field. // // Note that after normalisation, whole multiple of 60 minutes are added to // the number of hours, so the number of minutes will be reduced correspondingly. func (period Period) Minutes() int { return int(period.MinutesFloat()) } // MinutesFloat gets the number of minutes in the period. // The result is the number of minutes and does not include any other field. // // Note that after normalisation, whole multiple of 60 minutes are added to // the number of hours, so the number of minutes will be reduced correspondingly. func (period Period) MinutesFloat() float32 { return float32(period.minutes) / 10 } // Seconds gets the whole number of seconds in the period. // The result is the number of seconds and does not include any other field. // // Note that after normalisation, whole multiple of 60 seconds are added to // the number of minutes, so the number of seconds will be reduced correspondingly. func (period Period) Seconds() int { return int(period.SecondsFloat()) } // SecondsFloat gets the number of seconds in the period. // The result is the number of seconds and does not include any other field. // // Note that after normalisation, whole multiple of 60 seconds are added to // the number of minutes, so the number of seconds will be reduced correspondingly. func (period Period) SecondsFloat() float32 { return float32(period.seconds) / 10 } //------------------------------------------------------------------------------------------------- // 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.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 } // 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.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) 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 return hhE3 + mmE3 + ssE3 } func totalDaysApproxE7(period Period) int64 { // remember that the fields are all fixed-point 1E1 ydE6 := int64(period.years) * (daysPerYearE4 * 100) mdE6 := int64(period.months) * daysPerMonthE6 ddE6 := int64(period.days) * oneE6 return ydE6 + mdE6 + ddE6 } // 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) TotalDaysApprox() int { pn := period.Normalise(false) tdE6 := totalDaysApproxE7(pn) hE6 := (int64(pn.hours) * oneE6) / 24 return int((tdE6 + hE6) / oneE7) } // 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. func (period Period) TotalMonthsApprox() int { pn := period.Normalise(false) mE1 := int64(pn.years)*12 + int64(pn.months) hE1 := int64(pn.hours) / 24 dE1 := ((int64(pn.days) + hE1) * oneE6) / daysPerMonthE6 return int((mE1 + dE1) / 10) } // Normalise attempts to simplify the fields. It operates in either precise or imprecise mode. // // 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, or to transfer days to or from the months // element. To give control over this, there are two modes. // // In precise mode: // Multiples of 60 seconds become minutes. // Multiples of 60 minutes become hours. // Multiples of 12 months become years. // // Additionally, in imprecise mode: // Multiples of 24 hours become days. // Multiples of approx. 30.4 days become months. // // Note that leap seconds are disregarded: every minute is assumed to have 60 seconds. func (period Period) Normalise(precise bool) Period { n, _ := period.toPeriod64("").normalise64(precise).toPeriod() return n } // Simplify applies some heuristic simplifications with the objective of reducing the number // of non-zero fields and thus making the rendered form simpler. It should be applied to // a normalised period, otherwise the results may be unpredictable. // // Note that months and days are never combined, due to the variability of month lengths. // Days and hours are only combined when imprecise behaviour is selected; this is due to // daylight savings transitions, during which there are more than or fewer than 24 hours // per day. // // The following transformation rules are applied in order: // // * P1YnM becomes 12+n months for 0 < n <= 6 // * P1DTnH becomes 24+n hours for 0 < n <= 6 (unless precise is true) // * PT1HnM becomes 60+n minutes for 0 < n <= 10 // * PT1MnS becomes 60+n seconds for 0 < n <= 10 // // At each step, if a fraction exists and would affect the calculation, the transformations // stop. Also, when not precise, // // * for periods of at least ten years, month proper fractions are discarded // * for periods of at least a year, day proper fractions are discarded // * for periods of at least a month, hour proper fractions are discarded // * for periods of at least a day, minute proper fractions are discarded // * for periods of at least an hour, second proper fractions are discarded // // The thresholds can be set using the varargs th parameter. By default, the thresholds a, // b, c, d are 6 months, 6 hours, 10 minutes, 10 seconds respectively as listed in the rules // above. // // * No thresholds is equivalent to 6, 6, 10, 10. // * A single threshold a is equivalent to a, a, a, a. // * Two thresholds a, b are equivalent to a, a, b, b. // * Three thresholds a, b, c are equivalent to a, b, c, c. // * Four thresholds a, b, c, d are used as provided. // func (period Period) Simplify(precise bool, th ...int) Period { switch len(th) { case 0: return period.doSimplify(precise, 60, 60, 100, 100) case 1: return period.doSimplify(precise, int16(th[0]*10), int16(th[0]*10), int16(th[0]*10), int16(th[0]*10)) case 2: return period.doSimplify(precise, int16(th[0]*10), int16(th[0]*10), int16(th[1]*10), int16(th[1]*10)) case 3: return period.doSimplify(precise, int16(th[0]*10), int16(th[1]*10), int16(th[2]*10), int16(th[2]*10)) default: return period.doSimplify(precise, int16(th[0]*10), int16(th[1]*10), int16(th[2]*10), int16(th[3]*10)) } } func (period Period) doSimplify(precise bool, a, b, c, d int16) Period { if period.years%10 != 0 { return period } ap, neg := period.absNeg() // single year is dropped if there are some months if ap.years == 10 && 0 < ap.months && ap.months <= a && ap.days == 0 { ap.months += 120 ap.years = 0 } if ap.months%10 != 0 { // month fraction is dropped for periods of at least ten years (1:120) months := ap.months / 10 if !precise && ap.years >= 100 && months == 0 { ap.months = 0 } return ap.condNegate(neg) } if ap.days%10 != 0 { // day fraction is dropped for periods of at least a year (1:365) days := ap.days / 10 if !precise && (ap.years > 0 || ap.months >= 120) && days == 0 { ap.days = 0 } return ap.condNegate(neg) } if !precise && ap.days == 10 && ap.years == 0 && ap.months == 0 && 0 < ap.hours && ap.hours <= b { ap.hours += 240 ap.days = 0 } if ap.hours%10 != 0 { // hour fraction is dropped for periods of at least a month (1:720) hours := ap.hours / 10 if !precise && (ap.years > 0 || ap.months > 0 || ap.days >= 300) && hours == 0 { ap.hours = 0 } return ap.condNegate(neg) } if ap.hours == 10 && 0 < ap.minutes && ap.minutes <= c { ap.minutes += 600 ap.hours = 0 } if ap.minutes%10 != 0 { // minute fraction is dropped for periods of at least a day (1:1440) minutes := ap.minutes / 10 if !precise && (ap.years > 0 || ap.months > 0 || ap.days > 0 || ap.hours >= 240) && minutes == 0 { ap.minutes = 0 } return ap.condNegate(neg) } if ap.minutes == 10 && ap.hours == 0 && 0 < ap.seconds && ap.seconds <= d { ap.seconds += 600 ap.minutes = 0 } if ap.seconds%10 != 0 { // second fraction is dropped for periods of at least an hour (1:3600) seconds := ap.seconds / 10 if !precise && (ap.years > 0 || ap.months > 0 || ap.days > 0 || ap.hours > 0 || ap.minutes >= 600) && seconds == 0 { ap.seconds = 0 } } return ap.condNegate(neg) } golang-github-rickb777-date-1.15.3/period/period64.go000066400000000000000000000071051410771016400220510ustar00rootroot00000000000000package period import ( "fmt" "math" "strings" ) // used for stages in arithmetic type period64 struct { // always positive values years, months, days, hours, minutes, seconds int64 // true if the period is negative neg bool input string } func (period Period) toPeriod64(input string) *period64 { if period.IsNegative() { return &period64{ years: int64(-period.years), months: int64(-period.months), days: int64(-period.days), hours: int64(-period.hours), minutes: int64(-period.minutes), seconds: int64(-period.seconds), neg: true, input: input, } } return &period64{ years: int64(period.years), months: int64(period.months), days: int64(period.days), hours: int64(period.hours), minutes: int64(period.minutes), seconds: int64(period.seconds), input: input, } } func (p64 *period64) toPeriod() (Period, error) { var f []string if p64.years > math.MaxInt16 { f = append(f, "years") } if p64.months > math.MaxInt16 { f = append(f, "months") } if p64.days > math.MaxInt16 { f = append(f, "days") } if p64.hours > math.MaxInt16 { f = append(f, "hours") } if p64.minutes > math.MaxInt16 { f = append(f, "minutes") } if p64.seconds > math.MaxInt16 { f = append(f, "seconds") } if len(f) > 0 { if p64.input == "" { p64.input = p64.String() } return Period{}, fmt.Errorf("%s: integer overflow occurred in %s", p64.input, strings.Join(f, ",")) } if p64.neg { return Period{ int16(-p64.years), int16(-p64.months), int16(-p64.days), int16(-p64.hours), int16(-p64.minutes), int16(-p64.seconds), }, nil } return Period{ int16(p64.years), int16(p64.months), int16(p64.days), int16(p64.hours), int16(p64.minutes), int16(p64.seconds), }, nil } func (p64 *period64) normalise64(precise bool) *period64 { return p64.rippleUp(precise).moveFractionToRight() } func (p64 *period64) rippleUp(precise bool) *period64 { // remember that the fields are all fixed-point 1E1 p64.minutes += (p64.seconds / 600) * 10 p64.seconds = p64.seconds % 600 p64.hours += (p64.minutes / 600) * 10 p64.minutes = p64.minutes % 600 // 32670-(32670/60)-(32670/3600) = 32760 - 546 - 9.1 = 32204.9 if !precise || p64.hours > 32204 { p64.days += (p64.hours / 240) * 10 p64.hours = p64.hours % 240 } if !precise || p64.days > 32760 { dE6 := p64.days * oneE5 p64.months += (dE6 / daysPerMonthE6) * 10 p64.days = (dE6 % daysPerMonthE6) / oneE5 } p64.years += (p64.months / 120) * 10 p64.months = p64.months % 120 return p64 } // moveFractionToRight attempts to remove fractions in higher-order fields by moving their value to the // next-lower-order field. For example, fractional years become months. func (p64 *period64) moveFractionToRight() *period64 { // remember that the fields are all fixed-point 1E1 y10 := p64.years % 10 if y10 != 0 && (p64.months != 0 || p64.days != 0 || p64.hours != 0 || p64.minutes != 0 || p64.seconds != 0) { p64.months += y10 * 12 p64.years = (p64.years / 10) * 10 } m10 := p64.months % 10 if m10 != 0 && (p64.days != 0 || p64.hours != 0 || p64.minutes != 0 || p64.seconds != 0) { p64.days += (m10 * daysPerMonthE6) / oneE6 p64.months = (p64.months / 10) * 10 } d10 := p64.days % 10 if d10 != 0 && (p64.hours != 0 || p64.minutes != 0 || p64.seconds != 0) { p64.hours += d10 * 24 p64.days = (p64.days / 10) * 10 } hh10 := p64.hours % 10 if hh10 != 0 && (p64.minutes != 0 || p64.seconds != 0) { p64.minutes += hh10 * 60 p64.hours = (p64.hours / 10) * 10 } mm10 := p64.minutes % 10 if mm10 != 0 && p64.seconds != 0 { p64.seconds += mm10 * 60 p64.minutes = (p64.minutes / 10) * 10 } return p64 } golang-github-rickb777-date-1.15.3/period/period_test.go000066400000000000000000001141111410771016400227320ustar00rootroot00000000000000// 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 period import ( "fmt" "strings" "testing" "time" . "github.com/onsi/gomega" ) var oneDay = 24 * time.Hour var oneMonthApprox = 2629746 * time.Second // 30.436875 days var oneYearApprox = 31556952 * time.Second // 365.2425 days func TestParseErrors(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value string normalise bool expected string expvalue string }{ {"", false, "cannot parse a blank string as a period", ""}, {`P000`, false, `: missing designator at the end`, "P000"}, {"XY", false, ": expected 'P' period mark at the start", "XY"}, {"PxY", false, ": expected a number but found 'x'", "PxY"}, {"PxW", false, ": expected a number but found 'x'", "PxW"}, {"PxD", false, ": expected a number but found 'x'", "PxD"}, {"PTxH", false, ": expected a number but found 'x'", "PTxH"}, {"PTxM", false, ": expected a number but found 'x'", "PTxM"}, {"PTxS", false, ": expected a number but found 'x'", "PTxS"}, {"P1HT1M", false, ": 'H' designator cannot occur here", "P1HT1M"}, {"PT1Y", false, ": 'Y' designator cannot occur here", "PT1Y"}, {"P1S", false, ": 'S' designator cannot occur here", "P1S"}, {"P1D2D", false, ": 'D' designator cannot occur more than once", "P1D2D"}, {"PT1HT1S", false, ": 'T' designator cannot occur more than once", "PT1HT1S"}, {"P0.1YT0.1S", false, ": 'Y' & 'S' only the last field can have a fraction", "P0.1YT0.1S"}, {"P", false, ": expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' designator", "P"}, // integer overflow {"P32768Y", false, ": integer overflow occurred in years", "P32768Y"}, {"P32768M", false, ": integer overflow occurred in months", "P32768M"}, {"P32768W", false, ": integer overflow occurred in days", "P32768W"}, {"P32768D", false, ": integer overflow occurred in days", "P32768D"}, {"PT32768H", false, ": integer overflow occurred in hours", "PT32768H"}, {"PT32768M", false, ": integer overflow occurred in minutes", "PT32768M"}, {"PT32768S", false, ": integer overflow occurred in seconds", "PT32768S"}, {"PT32768H32768M32768S", false, ": integer overflow occurred in hours,minutes,seconds", "PT32768H32768M32768S"}, {"PT103412160000S", false, ": integer overflow occurred in seconds", "PT103412160000S"}, } for i, c := range cases { _, ep := Parse(c.value, c.normalise) g.Expect(ep).To(HaveOccurred(), info(i, c.value)) g.Expect(ep.Error()).To(Equal(c.expvalue+c.expected), info(i, c.value)) _, en := Parse("-"+c.value, c.normalise) g.Expect(en).To(HaveOccurred(), info(i, c.value)) if c.expvalue != "" { g.Expect(en.Error()).To(Equal("-"+c.expvalue+c.expected), info(i, c.value)) } else { g.Expect(en.Error()).To(Equal(c.expected), info(i, c.value)) } } } //------------------------------------------------------------------------------------------------- func TestParsePeriodWithNormalise(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value string reversed string period Period }{ // all rollovers {"PT1234.5S", "PT20M34.5S", Period{minutes: 200, seconds: 345}}, {"PT1234.5M", "PT20H34.5M", Period{hours: 200, minutes: 345}}, {"PT12345.6H", "P514DT9.6H", Period{days: 5140, hours: 96}}, {"P3277D", "P8Y11M20.2D", Period{years: 80, months: 110, days: 202}}, {"P1234.5M", "P102Y10.5M", Period{years: 1020, months: 105}}, // largest possible number of seconds normalised only in hours, mins, sec {"PT11592000S", "PT3220H", Period{hours: 32200}}, {"-PT11592000S", "-PT3220H", Period{hours: -32200}}, {"PT11595599S", "PT3220H59M59S", Period{hours: 32200, minutes: 590, seconds: 590}}, // largest possible number of seconds normalised only in days, hours, mins, sec {"PT283046400S", "P468W", Period{days: 32760}}, {"-PT283046400S", "-P468W", Period{days: -32760}}, {"PT43084443590S", "P1365Y3M2WT26H83M50S", Period{years: 13650, months: 30, days: 140, hours: 260, minutes: 830, seconds: 500}}, {"PT103412159999S", "P3276Y11M29DT39H107M59S", Period{years: 32760, months: 110, days: 290, hours: 390, minutes: 1070, seconds: 590}}, {"PT283132799S", "P468WT23H59M59S", Period{days: 32760, hours: 230, minutes: 590, seconds: 590}}, // other examples are in TestNormalise } for i, c := range cases { p, err := Parse(c.value) s := info(i, c.value) g.Expect(err).NotTo(HaveOccurred(), s) expectValid(t, p, s) g.Expect(p).To(Equal(c.period), s) // reversal is expected not to be an identity g.Expect(p.String()).To(Equal(c.reversed), s+" reversed") } } //------------------------------------------------------------------------------------------------- func TestParsePeriodWithoutNormalise(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value string reversed string period Period }{ // zero {"P0D", "P0D", Period{}}, // special zero cases: parse is not identity when reversed {"P0", "P0D", Period{}}, {"P0Y", "P0D", Period{}}, {"P0M", "P0D", Period{}}, {"P0W", "P0D", Period{}}, {"PT0H", "P0D", Period{}}, {"PT0M", "P0D", Period{}}, {"PT0S", "P0D", Period{}}, // ones {"P1Y", "P1Y", Period{years: 10}}, {"P1M", "P1M", Period{months: 10}}, {"P1W", "P1W", Period{days: 70}}, {"P1D", "P1D", Period{days: 10}}, {"PT1H", "PT1H", Period{hours: 10}}, {"PT1M", "PT1M", Period{minutes: 10}}, {"PT1S", "PT1S", Period{seconds: 10}}, // smallest {"P0.1Y", "P0.1Y", Period{years: 1}}, {"-P0.1Y", "-P0.1Y", Period{years: -1}}, {"P0.1M", "P0.1M", Period{months: 1}}, {"-P0.1M", "-P0.1M", Period{months: -1}}, {"P0.1D", "P0.1D", Period{days: 1}}, {"-P0.1D", "-P0.1D", Period{days: -1}}, {"PT0.1H", "PT0.1H", Period{hours: 1}}, {"-PT0.1H", "-PT0.1H", Period{hours: -1}}, {"PT0.1M", "PT0.1M", Period{minutes: 1}}, {"-PT0.1M", "-PT0.1M", Period{minutes: -1}}, {"PT0.1S", "PT0.1S", Period{seconds: 1}}, {"-PT0.1S", "-PT0.1S", Period{seconds: -1}}, // week special case: also not identity when reversed {"P0.1W", "P0.7D", Period{days: 7}}, {"-P0.1W", "-P0.7D", Period{days: -7}}, // largest {"PT3276.7S", "PT3276.7S", Period{seconds: 32767}}, {"PT3276.7M", "PT3276.7M", Period{minutes: 32767}}, {"PT3276.7H", "PT3276.7H", Period{hours: 32767}}, {"P3276.7D", "P3276.7D", Period{days: 32767}}, {"P3276.7M", "P3276.7M", Period{months: 32767}}, {"P3276.7Y", "P3276.7Y", Period{years: 32767}}, {"P3Y", "P3Y", Period{years: 30}}, {"P6M", "P6M", Period{months: 60}}, {"P5W", "P5W", Period{days: 350}}, {"P4D", "P4D", Period{days: 40}}, {"PT12H", "PT12H", Period{hours: 120}}, {"PT30M", "PT30M", Period{minutes: 300}}, {"PT25S", "PT25S", Period{seconds: 250}}, {"PT30M67.6S", "PT30M67.6S", Period{minutes: 300, seconds: 676}}, {"P2.Y", "P2Y", Period{years: 20}}, {"P2.5Y", "P2.5Y", Period{years: 25}}, {"P2.15Y", "P2.1Y", Period{years: 21}}, {"P1Y2.M", "P1Y2M", Period{years: 10, months: 20}}, {"P1Y2.5M", "P1Y2.5M", Period{years: 10, months: 25}}, {"P1Y2.15M", "P1Y2.1M", Period{years: 10, months: 21}}, // others {"P3Y6M5W4DT12H40M5S", "P3Y6M39DT12H40M5S", Period{years: 30, months: 60, days: 390, hours: 120, minutes: 400, seconds: 50}}, {"+P3Y6M5W4DT12H40M5S", "P3Y6M39DT12H40M5S", Period{years: 30, months: 60, days: 390, hours: 120, minutes: 400, seconds: 50}}, {"-P3Y6M5W4DT12H40M5S", "-P3Y6M39DT12H40M5S", Period{years: -30, months: -60, days: -390, hours: -120, minutes: -400, seconds: -50}}, {"P1Y14M35DT48H125M800S", "P1Y14M5WT48H125M800S", Period{years: 10, months: 140, days: 350, hours: 480, minutes: 1250, seconds: 8000}}, } for i, c := range cases { p, err := Parse(c.value, false) s := info(i, c.value) g.Expect(err).NotTo(HaveOccurred(), s) expectValid(t, p, s) g.Expect(p).To(Equal(c.period), s) // reversal is usually expected to be an identity g.Expect(p.String()).To(Equal(c.reversed), s+" reversed") } } //------------------------------------------------------------------------------------------------- func TestPeriodString(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value string period Period }{ // note: the negative cases are also covered (see below) {"P0D", Period{}}, // ones {"P1Y", Period{years: 10}}, {"P1M", Period{months: 10}}, {"P1W", Period{days: 70}}, {"P1D", Period{days: 10}}, {"PT1H", Period{hours: 10}}, {"PT1M", Period{minutes: 10}}, {"PT1S", Period{seconds: 10}}, // smallest {"P0.1Y", Period{years: 1}}, {"P0.1M", Period{months: 1}}, {"P0.7D", Period{days: 7}}, {"P0.1D", Period{days: 1}}, {"PT0.1H", Period{hours: 1}}, {"PT0.1M", Period{minutes: 1}}, {"PT0.1S", Period{seconds: 1}}, {"P3Y", Period{years: 30}}, {"P6M", Period{months: 60}}, {"P5W", Period{days: 350}}, {"P4W", Period{days: 280}}, {"P4D", Period{days: 40}}, {"PT12H", Period{hours: 120}}, {"PT30M", Period{minutes: 300}}, {"PT5S", Period{seconds: 50}}, {"P3Y6M39DT1H2M4.9S", Period{years: 30, months: 60, days: 390, hours: 10, minutes: 20, seconds: 49}}, {"P2.5Y", Period{years: 25}}, {"P2.5M", Period{months: 25}}, {"P2.5D", Period{days: 25}}, {"PT2.5H", Period{hours: 25}}, {"PT2.5M", Period{minutes: 25}}, {"PT2.5S", Period{seconds: 25}}, } for i, c := range cases { sp := c.period.String() g.Expect(sp).To(Equal(c.value), info(i, c.value)) if !c.period.IsZero() { sn := c.period.Negate().String() g.Expect(sn).To(Equal("-"+c.value), info(i, c.value)) } } } //------------------------------------------------------------------------------------------------- func TestPeriodIntComponents(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value string y, m, w, d, dx, hh, mm, ss int }{ // note: the negative cases are also covered (see below) {value: "P0D"}, {value: "P1Y", y: 1}, {value: "P1W", w: 1, d: 7}, {value: "P6M", m: 6}, {value: "P12M", m: 12}, {value: "P39D", w: 5, d: 39, dx: 4}, {value: "P4D", d: 4, dx: 4}, {value: "PT12H", hh: 12}, {value: "PT60M", mm: 60}, {value: "PT30M", mm: 30}, {value: "PT5S", ss: 5}, } for i, c := range cases { pp := MustParse(c.value, false) g.Expect(pp.Years()).To(Equal(c.y), info(i, pp)) g.Expect(pp.Months()).To(Equal(c.m), info(i, pp)) g.Expect(pp.Weeks()).To(Equal(c.w), info(i, pp)) g.Expect(pp.Days()).To(Equal(c.d), info(i, pp)) g.Expect(pp.ModuloDays()).To(Equal(c.dx), info(i, pp)) g.Expect(pp.Hours()).To(Equal(c.hh), info(i, pp)) g.Expect(pp.Minutes()).To(Equal(c.mm), info(i, pp)) g.Expect(pp.Seconds()).To(Equal(c.ss), info(i, pp)) pn := pp.Negate() g.Expect(pn.Years()).To(Equal(-c.y), info(i, pn)) g.Expect(pn.Months()).To(Equal(-c.m), info(i, pn)) g.Expect(pn.Weeks()).To(Equal(-c.w), info(i, pn)) g.Expect(pn.Days()).To(Equal(-c.d), info(i, pn)) g.Expect(pn.ModuloDays()).To(Equal(-c.dx), info(i, pn)) g.Expect(pn.Hours()).To(Equal(-c.hh), info(i, pn)) g.Expect(pn.Minutes()).To(Equal(-c.mm), info(i, pn)) g.Expect(pn.Seconds()).To(Equal(-c.ss), info(i, pn)) } } //------------------------------------------------------------------------------------------------- func TestPeriodFloatComponents(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value string y, m, w, d, dx, hh, mm, ss float32 }{ // note: the negative cases are also covered (see below) {value: "P0"}, // zero case // YMD cases {value: "P1Y", y: 1}, {value: "P1.5Y", y: 1.5}, {value: "P1.1Y", y: 1.1}, {value: "P1M", m: 1}, {value: "P1.5M", m: 1.5}, {value: "P1.1M", m: 1.1}, {value: "P6M", m: 6}, {value: "P12M", m: 12}, {value: "P7D", w: 1, d: 7}, {value: "P7.7D", w: 1.1, d: 7.7}, {value: "P7.1D", w: 7.1 / 7, d: 7.1}, {value: "P1D", w: 1.0 / 7, d: 1}, {value: "P1.1D", w: 1.1 / 7, d: 1.1}, {value: "P1.1D", w: 1.1 / 7, d: 1.1}, {value: "P39D", w: 5.571429, d: 39, dx: 4}, {value: "P4D", w: 0.5714286, d: 4, dx: 4}, // HMS cases {value: "PT1.1H", hh: 1.1}, {value: "PT1H6M", hh: 1, mm: 6}, {value: "PT12H", hh: 12}, {value: "PT1.1M", mm: 1.1}, {value: "PT1M6S", mm: 1, ss: 6}, {value: "PT30M", mm: 30}, {value: "PT1.1S", ss: 1.1}, {value: "PT5S", ss: 5}, } for i, c := range cases { pp := MustParse(c.value, false) g.Expect(pp.YearsFloat()).To(Equal(c.y), info(i, pp)) g.Expect(pp.MonthsFloat()).To(Equal(c.m), info(i, pp)) g.Expect(pp.WeeksFloat()).To(Equal(c.w), info(i, pp)) g.Expect(pp.DaysFloat()).To(Equal(c.d), info(i, pp)) g.Expect(pp.HoursFloat()).To(Equal(c.hh), info(i, pp)) g.Expect(pp.MinutesFloat()).To(Equal(c.mm), info(i, pp)) g.Expect(pp.SecondsFloat()).To(Equal(c.ss), info(i, pp)) pn := pp.Negate() g.Expect(pn.YearsFloat()).To(Equal(-c.y), info(i, pn)) g.Expect(pn.MonthsFloat()).To(Equal(-c.m), info(i, pn)) g.Expect(pn.WeeksFloat()).To(Equal(-c.w), info(i, pn)) g.Expect(pn.DaysFloat()).To(Equal(-c.d), info(i, pn)) g.Expect(pn.HoursFloat()).To(Equal(-c.hh), info(i, pn)) g.Expect(pn.MinutesFloat()).To(Equal(-c.mm), info(i, pn)) g.Expect(pn.SecondsFloat()).To(Equal(-c.ss), info(i, pn)) } } //------------------------------------------------------------------------------------------------- func TestPeriodToDuration(t *testing.T) { cases := []struct { value string duration time.Duration precise bool }{ // note: the negative cases are also covered (see below) {"P0D", time.Duration(0), true}, {"PT1S", 1 * time.Second, true}, {"PT0.1S", 100 * time.Millisecond, true}, {"PT3276S", 3276 * time.Second, true}, {"PT1M", 60 * time.Second, true}, {"PT0.1M", 6 * time.Second, true}, {"PT3276M", 3276 * time.Minute, true}, {"PT1H", 3600 * time.Second, true}, {"PT0.1H", 360 * time.Second, true}, {"PT3220H", 3220 * time.Hour, true}, // days, months and years conversions are never precise {"P1D", 24 * time.Hour, false}, {"P0.1D", 144 * time.Minute, false}, {"P3276D", 3276 * 24 * time.Hour, false}, {"P1M", oneMonthApprox, false}, {"P0.1M", oneMonthApprox / 10, false}, {"P3276M", 3276 * oneMonthApprox, false}, {"P1Y", oneYearApprox, false}, {"P3276Y", 3276 * oneYearApprox, false}, // near the upper limit of range } for i, c := range cases { testPeriodToDuration(t, i, c.value, c.duration, c.precise) testPeriodToDuration(t, i, "-"+c.value, -c.duration, c.precise) } } func testPeriodToDuration(t *testing.T, i int, value string, duration time.Duration, precise bool) { t.Helper() g := NewGomegaWithT(t) hint := info(i, "%s %s %v", value, duration, precise) pp := MustParse(value, false) d1, prec := pp.Duration() g.Expect(d1).To(Equal(duration), hint) g.Expect(prec).To(Equal(precise), hint) d2 := pp.DurationApprox() if precise { g.Expect(d2).To(Equal(duration), hint) } } //------------------------------------------------------------------------------------------------- func TestSignPositiveNegative(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value string positive bool negative bool sign int }{ {"P0D", false, false, 0}, {"PT1S", true, false, 1}, {"PT0.1S", true, false, 1}, {"-PT1S", false, true, -1}, {"-PT0.1S", false, true, -1}, {"PT1M", true, false, 1}, {"PT0.1M", true, false, 1}, {"-PT1M", false, true, -1}, {"-PT0.1M", false, true, -1}, {"PT1H", true, false, 1}, {"PT0.1H", true, false, 1}, {"-PT1H", false, true, -1}, {"-PT0.1H", false, true, -1}, {"P1D", true, false, 1}, {"P10.D", true, false, 1}, {"-P1D", false, true, -1}, {"-P0.1D", false, true, -1}, {"P1M", true, false, 1}, {"P0.1M", true, false, 1}, {"-P1M", false, true, -1}, {"-P0.1M", false, true, -1}, {"P1Y", true, false, 1}, {"P0.1Y", true, false, 1}, {"-P1Y", false, true, -1}, {"-P0.1Y", false, true, -1}, } for i, c := range cases { p := MustParse(c.value, false) g.Expect(p.IsPositive()).To(Equal(c.positive), info(i, c.value)) g.Expect(p.IsNegative()).To(Equal(c.negative), info(i, c.value)) g.Expect(p.Sign()).To(Equal(c.sign), info(i, c.value)) } } //------------------------------------------------------------------------------------------------- func TestPeriodApproxDays(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value string approxDays int }{ // note: the negative cases are also covered (see below) {"P0D", 0}, {"PT24H", 1}, {"PT49H", 2}, {"P1D", 1}, {"P1M", 30}, {"P1Y", 365}, } for i, c := range cases { p := MustParse(c.value, false) td1 := p.TotalDaysApprox() g.Expect(td1).To(Equal(c.approxDays), info(i, c.value)) td2 := p.Negate().TotalDaysApprox() g.Expect(td2).To(Equal(-c.approxDays), info(i, c.value)) } } //------------------------------------------------------------------------------------------------- func TestPeriodApproxMonths(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { value string approxMonths int }{ // note: the negative cases are also covered (see below) {"P0D", 0}, {"P1D", 0}, {"P30D", 0}, {"P31D", 1}, {"P60D", 1}, {"P62D", 2}, {"P1M", 1}, {"P12M", 12}, {"P2M31D", 3}, {"P1Y", 12}, {"P2Y3M", 27}, {"PT24H", 0}, {"PT744H", 1}, } for i, c := range cases { p := MustParse(c.value, false) td1 := p.TotalMonthsApprox() g.Expect(td1).To(Equal(c.approxMonths), info(i, c.value)) td2 := p.Negate().TotalMonthsApprox() g.Expect(td2).To(Equal(-c.approxMonths), info(i, c.value)) } } //------------------------------------------------------------------------------------------------- func TestNewPeriod(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { period string years, months, days, hours, minutes, seconds int }{ // note: the negative cases are also covered (see below) {period: "P0"}, // zero case {period: "PT1S", seconds: 1}, {period: "PT1M", minutes: 1}, {period: "PT1H", hours: 1}, {period: "P1D", days: 1}, {period: "P1M", months: 1}, {period: "P1Y", years: 1}, {period: "P100Y222M700D", years: 100, months: 222, days: 700}, } for i, c := range cases { ep, _ := Parse(c.period, false) pp := New(c.years, c.months, c.days, c.hours, c.minutes, c.seconds) expectValid(t, pp, info(i, c.period)) g.Expect(pp).To(Equal(ep), info(i, c.period)) g.Expect(pp.Years()).To(Equal(c.years), info(i, c.period)) g.Expect(pp.Months()).To(Equal(c.months), info(i, c.period)) g.Expect(pp.Days()).To(Equal(c.days), info(i, c.period)) pn := New(-c.years, -c.months, -c.days, -c.hours, -c.minutes, -c.seconds) en := ep.Negate() expectValid(t, pn, info(i, en)) g.Expect(pn).To(Equal(en), info(i, en)) g.Expect(pn.Years()).To(Equal(-c.years), info(i, en)) g.Expect(pn.Months()).To(Equal(-c.months), info(i, en)) g.Expect(pn.Days()).To(Equal(-c.days), info(i, en)) } } //------------------------------------------------------------------------------------------------- func TestNewHMS(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { period Period hours, minutes, seconds int }{ // note: the negative cases are also covered (see below) {}, // zero case {period: Period{seconds: 10}, seconds: 1}, {period: Period{minutes: 10}, minutes: 1}, {period: Period{hours: 10}, hours: 1}, {period: Period{hours: 30, minutes: 40, seconds: 50}, hours: 3, minutes: 4, seconds: 5}, {period: Period{hours: 32760, minutes: 32760, seconds: 32760}, hours: 3276, minutes: 3276, seconds: 3276}, } for i, c := range cases { pp := NewHMS(c.hours, c.minutes, c.seconds) expectValid(t, pp, info(i, c.period)) g.Expect(pp).To(Equal(c.period), info(i, c.period)) g.Expect(pp.Hours()).To(Equal(c.hours), info(i, c.period)) g.Expect(pp.Minutes()).To(Equal(c.minutes), info(i, c.period)) g.Expect(pp.Seconds()).To(Equal(c.seconds), info(i, c.period)) pn := NewHMS(-c.hours, -c.minutes, -c.seconds) en := c.period.Negate() expectValid(t, pn, info(i, en)) g.Expect(pn).To(Equal(en), info(i, en)) g.Expect(pn.Hours()).To(Equal(-c.hours), info(i, en)) g.Expect(pn.Minutes()).To(Equal(-c.minutes), info(i, en)) g.Expect(pn.Seconds()).To(Equal(-c.seconds), info(i, en)) } } //------------------------------------------------------------------------------------------------- func TestNewYMD(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { period Period years, months, days int }{ // note: the negative cases are also covered (see below) {}, // zero case {period: Period{days: 10}, days: 1}, {period: Period{months: 10}, months: 1}, {period: Period{years: 10}, years: 1}, {period: Period{years: 1000, months: 2220, days: 7000}, years: 100, months: 222, days: 700}, {period: Period{years: 32760, months: 32760, days: 32760}, years: 3276, months: 3276, days: 3276}, } for i, c := range cases { pp := NewYMD(c.years, c.months, c.days) expectValid(t, pp, info(i, c.period)) g.Expect(pp).To(Equal(c.period), info(i, c.period)) g.Expect(pp.Years()).To(Equal(c.years), info(i, c.period)) g.Expect(pp.Months()).To(Equal(c.months), info(i, c.period)) g.Expect(pp.Days()).To(Equal(c.days), info(i, c.period)) pn := NewYMD(-c.years, -c.months, -c.days) en := c.period.Negate() expectValid(t, pn, info(i, en)) g.Expect(pn).To(Equal(en), info(i, en)) g.Expect(pn.Years()).To(Equal(-c.years), info(i, en)) g.Expect(pn.Months()).To(Equal(-c.months), info(i, en)) g.Expect(pn.Days()).To(Equal(-c.days), info(i, en)) } } //------------------------------------------------------------------------------------------------- func TestNewOf(t *testing.T) { // note: the negative cases are also covered (see below) // HMS tests testNewOf(t, 1, 100*time.Millisecond, Period{seconds: 1}, true) testNewOf(t, 2, time.Second, Period{seconds: 10}, true) testNewOf(t, 3, time.Minute, Period{minutes: 10}, true) testNewOf(t, 4, time.Hour, Period{hours: 10}, true) testNewOf(t, 5, time.Hour+time.Minute+time.Second, Period{hours: 10, minutes: 10, seconds: 10}, true) testNewOf(t, 6, 24*time.Hour+time.Minute+time.Second, Period{hours: 240, minutes: 10, seconds: 10}, true) testNewOf(t, 7, 3276*time.Hour+59*time.Minute+59*time.Second, Period{hours: 32760, minutes: 590, seconds: 590}, true) testNewOf(t, 8, 30*time.Minute+67*time.Second+600*time.Millisecond, Period{minutes: 310, seconds: 76}, true) // YMD tests: must be over 3276 hours (approx 4.5 months), otherwise HMS will take care of it // first rollover: >3276 hours testNewOf(t, 10, 3277*time.Hour, Period{days: 1360, hours: 130}, false) testNewOf(t, 11, 3288*time.Hour, Period{days: 1370}, false) testNewOf(t, 12, 3289*time.Hour, Period{days: 1370, hours: 10}, false) testNewOf(t, 13, 24*3276*time.Hour, Period{days: 32760}, false) // second rollover: >3276 days testNewOf(t, 14, 24*3277*time.Hour, Period{years: 80, months: 110, days: 200}, false) testNewOf(t, 15, 3277*oneDay, Period{years: 80, months: 110, days: 200}, false) testNewOf(t, 16, 3277*oneDay+time.Hour+time.Minute+time.Second, Period{years: 80, months: 110, days: 200, hours: 10}, false) testNewOf(t, 17, 36525*oneDay, Period{years: 1000}, false) } func testNewOf(t *testing.T, i int, source time.Duration, expected Period, precise bool) { t.Helper() testNewOf1(t, i, source, expected, precise) testNewOf1(t, i, -source, expected.Negate(), precise) } func testNewOf1(t *testing.T, i int, source time.Duration, expected Period, precise bool) { t.Helper() g := NewGomegaWithT(t) n, p := NewOf(source) rev, _ := expected.Duration() info := fmt.Sprintf("%d: source %v expected %+v precise %v rev %v", i, source, expected, precise, rev) expectValid(t, n, info) g.Expect(n).To(Equal(expected), info) g.Expect(p).To(Equal(precise), info) if precise { g.Expect(rev).To(Equal(source), info) } } //------------------------------------------------------------------------------------------------- func TestBetween(t *testing.T) { g := NewGomegaWithT(t) now := time.Now() cases := []struct { a, b time.Time expected Period }{ // note: the negative cases are also covered (see below) {now, now, Period{}}, // simple positive date calculations {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 1, 1, 0, 0, 0, 100), Period{seconds: 1}}, {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 2, 2, 1, 1, 1, 1), Period{days: 320, hours: 10, minutes: 10, seconds: 10}}, {utc(2015, 2, 1, 0, 0, 0, 0), utc(2015, 3, 2, 1, 1, 1, 1), Period{days: 290, hours: 10, minutes: 10, seconds: 10}}, {utc(2015, 3, 1, 0, 0, 0, 0), utc(2015, 4, 2, 1, 1, 1, 1), Period{days: 320, hours: 10, minutes: 10, seconds: 10}}, {utc(2015, 4, 1, 0, 0, 0, 0), utc(2015, 5, 2, 1, 1, 1, 1), Period{days: 310, hours: 10, minutes: 10, seconds: 10}}, {utc(2015, 5, 1, 0, 0, 0, 0), utc(2015, 6, 2, 1, 1, 1, 1), Period{days: 320, hours: 10, minutes: 10, seconds: 10}}, {utc(2015, 6, 1, 0, 0, 0, 0), utc(2015, 7, 2, 1, 1, 1, 1), Period{days: 310, hours: 10, minutes: 10, seconds: 10}}, {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 7, 2, 1, 1, 1, 1), Period{days: 1820, hours: 10, minutes: 10, seconds: 10}}, // less than one month {utc(2016, 1, 2, 0, 0, 0, 0), utc(2016, 2, 1, 0, 0, 0, 0), Period{days: 300}}, {utc(2015, 2, 2, 0, 0, 0, 0), utc(2015, 3, 1, 0, 0, 0, 0), Period{days: 270}}, // non-leap {utc(2016, 2, 2, 0, 0, 0, 0), utc(2016, 3, 1, 0, 0, 0, 0), Period{days: 280}}, // leap year {utc(2016, 3, 2, 0, 0, 0, 0), utc(2016, 4, 1, 0, 0, 0, 0), Period{days: 300}}, {utc(2016, 4, 2, 0, 0, 0, 0), utc(2016, 5, 1, 0, 0, 0, 0), Period{days: 290}}, {utc(2016, 5, 2, 0, 0, 0, 0), utc(2016, 6, 1, 0, 0, 0, 0), Period{days: 300}}, {utc(2016, 6, 2, 0, 0, 0, 0), utc(2016, 7, 1, 0, 0, 0, 0), Period{days: 290}}, // BST drops an hour at the daylight-saving transition {utc(2015, 1, 1, 0, 0, 0, 0), bst(2015, 7, 2, 1, 1, 1, 1), Period{days: 1820, minutes: 10, seconds: 10}}, // daytime only {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 2, 3, 4, 500), Period{seconds: 5}}, {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 4, 4, 7, 500), Period{hours: 20, minutes: 10, seconds: 35}}, {utc(2015, 1, 1, 2, 3, 4, 500), utc(2015, 1, 1, 4, 4, 7, 0), Period{hours: 20, minutes: 10, seconds: 25}}, // different dates and times {utc(2015, 2, 1, 1, 0, 0, 0), utc(2015, 5, 30, 5, 6, 7, 0), Period{days: 1180, hours: 40, minutes: 60, seconds: 70}}, {utc(2015, 2, 1, 1, 0, 0, 0), bst(2015, 5, 30, 5, 6, 7, 0), Period{days: 1180, hours: 30, minutes: 60, seconds: 70}}, // earlier month in later year {utc(2015, 12, 22, 0, 0, 0, 0), utc(2016, 1, 10, 5, 6, 7, 0), Period{days: 190, hours: 50, minutes: 60, seconds: 70}}, {utc(2015, 2, 11, 5, 6, 7, 500), utc(2016, 1, 10, 0, 0, 0, 0), Period{days: 3320, hours: 180, minutes: 530, seconds: 525}}, // larger ranges {utc(2009, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{days: 29210, seconds: 10}}, {utc(2008, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{years: 80, months: 110, days: 300, seconds: 10}}, {utc(1900, 1, 1, 0, 0, 1, 0), utc(2009, 12, 31, 0, 0, 2, 0), Period{years: 1090, months: 110, days: 300, seconds: 10}}, } for i, c := range cases { pp := Between(c.a, c.b) g.Expect(pp).To(Equal(c.expected), info(i, c.expected)) pn := Between(c.b, c.a) expectValid(t, pn, info(i, c.expected)) en := c.expected.Negate() g.Expect(pn).To(Equal(en), info(i, en)) } } //------------------------------------------------------------------------------------------------- func TestNormaliseUnchanged(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { source period64 }{ // note: the negative cases are also covered (see below) // zero case {period64{}}, {period64{years: 1}}, {period64{months: 1}}, {period64{days: 1}}, {period64{hours: 1}}, {period64{minutes: 1}}, {period64{seconds: 1}}, {period64{years: 10, months: 10, days: 10, hours: 10, minutes: 10, seconds: 11}}, {period64{days: 10, hours: 70}}, {period64{days: 10, hours: 10, minutes: 10}}, {period64{days: 10, hours: 10, seconds: 10}}, {period64{months: 10, days: 10, hours: 10}}, {period64{minutes: 10, seconds: 10}}, {period64{hours: 10, minutes: 10}}, {period64{years: 10, months: 7}}, {period64{months: 11}}, {period64{days: 11}}, {period64{hours: 11}}, {period64{minutes: 11}}, {period64{seconds: 11}}, // don't carry days to months // don't carry months to years } for i, c := range cases { p, err := c.source.toPeriod() g.Expect(err).NotTo(HaveOccurred()) testNormalise(t, i, c.source, p, true) testNormalise(t, i, c.source, p, false) c.source.neg = true testNormalise(t, i, c.source, p.Negate(), true) testNormalise(t, i, c.source, p.Negate(), false) } } //------------------------------------------------------------------------------------------------- func TestNormaliseChanged(t *testing.T) { cases := []struct { source period64 precise, approx string }{ // note: the negative cases are also covered (see below) // carry seconds to minutes //{source: period64{seconds: 600}, precise: "PT1M"}, //{source: period64{seconds: 700}, precise: "PT1M10S"}, //{source: period64{seconds: 6990}, precise: "PT11M39S"}, // carry minutes to hours //{source: period64{minutes: 700}, precise: "PT1H10M"}, //{source: period64{minutes: 6990}, precise: "PT11H39M"}, // carry hours to days //{source: period64{hours: 480}, precise: "PT48H", approx: "P2D"}, //{source: period64{hours: 490}, precise: "PT49H", approx: "P2D T1H"}, //{source: period64{hours: 32761}, precise: "PT3276.1H", approx: "P4M 14D T 16.9H"}, //{source: period64{years: 10, months: 20, days: 30, hours: 32767}, precise: "P1Y 2M 3D T3276.7H", approx: "P1Y 6M 17D T17.5H"}, //{source: period64{hours: 32768}, precise: "P136DT12.8H", approx: "P4M 14D T17.6H"}, //{source: period64{years: 10, months: 20, days: 30, hours: 32768}, precise: "P1Y 2M 139D T12.8H", approx: "P1Y 6M 17D T17.6H"}, // carry days to months //{source: period64{days: 310}, precise: "P31D", approx: "P1M 0.5D"}, {source: period64{days: 32760}, precise: "P3276D", approx: "P8Y 11M 19.2D"}, {source: period64{days: 32761}, precise: "P8Y 11M 19.3D"}, // carry months to years {source: period64{months: 120}, precise: "P1Y"}, {source: period64{months: 132}, precise: "P1Y 1.2M"}, {source: period64{months: 250}, precise: "P2Y 1M"}, // full ripple up {source: period64{months: 130, days: 310, hours: 240, minutes: 600, seconds: 611}, precise: "P1Y 1M 31D T25H 1M 1.1S", approx: "P1Y 2M 1D T13H 1M 1.1S"}, } for i, c := range cases { if c.approx == "" { c.approx = c.precise } pp := MustParse(nospace(c.precise), false) pa := MustParse(nospace(c.approx), false) testNormalise(t, i, c.source, pp, true) testNormalise(t, i, c.source, pa, false) c.source.neg = true testNormalise(t, i, c.source, pp.Negate(), true) testNormalise(t, i, c.source, pa.Negate(), false) } } func testNormalise(t *testing.T, i int, source period64, expected Period, precise bool) { g := NewGomegaWithT(t) t.Helper() sstr := source.String() n, err := source.normalise64(precise).toPeriod() info := fmt.Sprintf("%d: %s.Normalise(%v) expected %s to equal %s", i, sstr, precise, n, expected) expectValid(t, n, info) g.Expect(err).NotTo(HaveOccurred()) g.Expect(n).To(Equal(expected), info) if !precise { p1, _ := source.toPeriod() d1, pr1 := p1.Duration() d2, pr2 := expected.Duration() g.Expect(pr1).To(Equal(pr2), info) g.Expect(d1).To(Equal(d2), info) } } //------------------------------------------------------------------------------------------------- func TestPeriodFormat(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { period string expectW string // with weeks expectD string // without weeks }{ // note: the negative cases are also covered (see below) {"P0D", "0 days", ""}, {"P1Y1M7D", "1 year, 1 month, 1 week", "1 year, 1 month, 7 days"}, {"P1Y1M1W1D", "1 year, 1 month, 1 week, 1 day", "1 year, 1 month, 8 days"}, {"PT1H1M1S", "1 hour, 1 minute, 1 second", ""}, {"P1Y1M1W1DT1H1M1S", "1 year, 1 month, 1 week, 1 day, 1 hour, 1 minute, 1 second", ""}, {"P3Y6M39DT2H7M9S", "3 years, 6 months, 5 weeks, 4 days, 2 hours, 7 minutes, 9 seconds", ""}, {"P365D", "52 weeks, 1 day", ""}, {"P1Y", "1 year", ""}, {"P3Y", "3 years", ""}, {"P1.1Y", "1.1 years", ""}, {"P2.5Y", "2.5 years", ""}, {"P1M", "1 month", ""}, {"P6M", "6 months", ""}, {"P1.1M", "1.1 months", ""}, {"P2.5M", "2.5 months", ""}, {"P1W", "1 week", "7 days"}, {"P1.1W", "1 week, 0.7 day", "7.7 days"}, {"P7D", "1 week", "7 days"}, {"P35D", "5 weeks", "35 days"}, {"P1D", "1 day", "1 day"}, {"P4D", "4 days", "4 days"}, {"P1.1D", "1.1 days", ""}, {"PT1H", "1 hour", ""}, {"PT1.1H", "1.1 hours", ""}, {"PT1M", "1 minute", ""}, {"PT1.1M", "1.1 minutes", ""}, {"PT1S", "1 second", ""}, {"PT1.1S", "1.1 seconds", ""}, } for i, c := range cases { p := MustParse(c.period, false) sp := p.Format() g.Expect(sp).To(Equal(c.expectW), info(i, "%s -> %s", p, c.expectW)) en := p.Negate() sn := en.Format() g.Expect(sn).To(Equal(c.expectW), info(i, "%s -> %s", en, c.expectW)) if c.expectD != "" { s := MustParse(c.period, false).FormatWithoutWeeks() g.Expect(s).To(Equal(c.expectD), info(i, "%s -> %s", p, c.expectD)) } } } //------------------------------------------------------------------------------------------------- func TestPeriodOnlyYMD(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { one string expect string }{ {"P1Y2M3DT4H5M6S", "P1Y2M3D"}, {"-P6Y5M4DT3H2M1S", "-P6Y5M4D"}, } for i, c := range cases { s := MustParse(c.one, false).OnlyYMD() g.Expect(s).To(Equal(MustParse(c.expect, false)), info(i, c.expect)) } } func TestPeriodOnlyHMS(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { one string expect string }{ {"P1Y2M3DT4H5M6S", "PT4H5M6S"}, {"-P6Y5M4DT3H2M1S", "-PT3H2M1S"}, } for i, c := range cases { s := MustParse(c.one, false).OnlyHMS() g.Expect(s).To(Equal(MustParse(c.expect, false)), info(i, c.expect)) } } //------------------------------------------------------------------------------------------------- func TestSimplify(t *testing.T) { cases := []struct { source, precise, approx string }{ // note: the negative cases are also covered (see below) // simplify 1 year to months (a = 9) {source: "P1Y"}, {source: "P1Y10M"}, {source: "P1Y9M", precise: "P21M"}, {source: "P1Y8.9M", precise: "P20.9M"}, // simplify 1 day to hours (approx only) (b = 6) {source: "P1DT6H", precise: "P1DT6H", approx: "PT30H"}, {source: "P1DT7H"}, {source: "P1DT5.9H", precise: "P1DT5.9H", approx: "PT29.9H"}, // simplify 1 hour to minutes (c = 10) {source: "PT1H"}, {source: "PT1H21M"}, {source: "PT1H10M", precise: "PT70M"}, {source: "PT1H9.9M", precise: "PT69.9M"}, // simplify 1 minute to seconds (d = 30) {source: "PT1M"}, // unchanged {source: "PT1M31S"}, // ditto {source: "PT1M30S", precise: "PT90S"}, {source: "PT1M29.9S", precise: "PT89.9S"}, // fractional years don't simplify {source: "P1.1Y"}, // retained proper fractions {source: "P1Y0.1D"}, {source: "P12M0.1D"}, {source: "P1YT0.1H"}, {source: "P1MT0.1H"}, {source: "P1Y0.1M", precise: "P12.1M"}, {source: "P1DT0.1H", precise: "P1DT0.1H", approx: "PT24.1H"}, {source: "P1YT0.1M"}, {source: "P1MT0.1M"}, {source: "P1DT0.1M"}, // discard proper fractions - months {source: "P10Y0.1M", precise: "P10Y0.1M", approx: "P10Y"}, // discard proper fractions - days {source: "P1Y0.1D", precise: "P1Y0.1D", approx: "P1Y"}, {source: "P12M0.1D", precise: "P12M0.1D", approx: "P12M"}, // discard proper fractions - hours {source: "P1YT0.1H", precise: "P1YT0.1H", approx: "P1Y"}, {source: "P1MT0.1H", precise: "P1MT0.1H", approx: "P1M"}, {source: "P30DT0.1H", precise: "P30DT0.1H", approx: "P30D"}, // discard proper fractions - minutes {source: "P1YT0.1M", precise: "P1YT0.1M", approx: "P1Y"}, {source: "P1MT0.1M", precise: "P1MT0.1M", approx: "P1M"}, {source: "P1DT0.1M", precise: "P1DT0.1M", approx: "P1D"}, {source: "PT24H0.1M", precise: "PT24H0.1M", approx: "PT24H"}, // discard proper fractions - seconds {source: "P1YT0.1S", precise: "P1YT0.1S", approx: "P1Y"}, {source: "P1MT0.1S", precise: "P1MT0.1S", approx: "P1M"}, {source: "P1DT0.1S", precise: "P1DT0.1S", approx: "P1D"}, {source: "PT1H0.1S", precise: "PT1H0.1S", approx: "PT1H"}, {source: "PT60M0.1S", precise: "PT60M0.1S", approx: "PT60M"}, } for i, c := range cases { p := MustParse(nospace(c.source), false) if c.precise == "" { // unchanged cases testSimplify(t, i, p, p, true) testSimplify(t, i, p.Negate(), p.Negate(), true) } else if c.approx == "" { // changed but precise/approx has same result ep := MustParse(nospace(c.precise), false) testSimplify(t, i, p, ep, true) testSimplify(t, i, p.Negate(), ep.Negate(), true) } else { // changed and precise/approx have different results ep := MustParse(nospace(c.precise), false) ea := MustParse(nospace(c.approx), false) testSimplify(t, i, p, ep, true) testSimplify(t, i, p.Negate(), ep.Negate(), true) testSimplify(t, i, p, ea, false) testSimplify(t, i, p.Negate(), ea.Negate(), false) } } g := NewGomegaWithT(t) g.Expect(Period{days: 10, hours: 70}.Simplify(false, 6, 7, 30)).To(Equal(Period{hours: 310})) g.Expect(Period{hours: 10, minutes: 300}.Simplify(true, 6, 30)).To(Equal(Period{minutes: 900})) g.Expect(Period{years: 10, months: 110}.Simplify(true, 11)).To(Equal(Period{months: 230})) g.Expect(Period{days: 10, hours: 60}.Simplify(false)).To(Equal(Period{hours: 300})) } func testSimplify(t *testing.T, i int, source Period, expected Period, precise bool) { g := NewGomegaWithT(t) t.Helper() sstr := source.String() n := source.Simplify(precise, 9, 6, 10, 30) info := fmt.Sprintf("%d: %s.Simplify(%v) expected %s to equal %s", i, sstr, precise, n, expected) expectValid(t, n, info) g.Expect(n).To(Equal(expected), info) } //------------------------------------------------------------------------------------------------- func utc(year int, month time.Month, day, hour, min, sec, msec int) time.Time { return time.Date(year, month, day, hour, min, sec, msec*int(time.Millisecond), time.UTC) } func bst(year int, month time.Month, day, hour, min, sec, msec int) time.Time { return time.Date(year, month, day, hour, min, sec, msec*int(time.Millisecond), london) } var london *time.Location // UTC + 1 hour during summer func init() { london, _ = time.LoadLocation("Europe/London") } func info(i int, m ...interface{}) string { if s, ok := m[0].(string); ok { m[0] = i return fmt.Sprintf("%d "+s, m...) } return fmt.Sprintf("%d %v", i, m[0]) } func nospace(s string) string { b := new(strings.Builder) for _, r := range s { if r != ' ' { b.WriteRune(r) } } return b.String() } golang-github-rickb777-date-1.15.3/period/sql.go000066400000000000000000000016031410771016400212110ustar00rootroot00000000000000// 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 period import ( "database/sql/driver" "fmt" ) // Scan parses some value, which can be either string or []byte. // It implements sql.Scanner, https://golang.org/pkg/database/sql/#Scanner func (period *Period) Scan(value interface{}) (err error) { if value == nil { return nil } err = nil switch v := value.(type) { case []byte: *period, err = Parse(string(v), false) case string: *period, err = Parse(v, false) default: err = fmt.Errorf("%T %+v is not a meaningful period", value, value) } return err } // Value converts the period to a string. It implements driver.Valuer, // https://golang.org/pkg/database/sql/driver/#Valuer func (period Period) Value() (driver.Value, error) { return period.String(), nil } golang-github-rickb777-date-1.15.3/period/sql_test.go000066400000000000000000000024101410771016400222450ustar00rootroot00000000000000// 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 period import ( "database/sql/driver" "testing" . "github.com/onsi/gomega" ) func TestPeriodScan(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { v interface{} expected Period }{ {[]byte("P1Y3M"), MustParse("P1Y3M", false)}, {"P1Y3M", MustParse("P1Y3M", false)}, // normalise should be disabled so that the retrieved value exactly // matches the stored value {[]byte("P48M"), MustParse("P48M", false)}, {"P48M", MustParse("P48M", false)}, } for _, c := range cases { r := new(Period) e := r.Scan(c.v) g.Expect(e).NotTo(HaveOccurred()) g.Expect(*r).To(Equal(c.expected)) var d driver.Valuer = *r q, e := d.Value() g.Expect(e).NotTo(HaveOccurred()) g.Expect(q.(string)).To(Equal(c.expected.String())) } } func TestPeriodScan_nil_value(t *testing.T) { g := NewGomegaWithT(t) r := new(Period) e := r.Scan(nil) g.Expect(e).NotTo(HaveOccurred()) } func TestPeriodScan_problem_type(t *testing.T) { g := NewGomegaWithT(t) r := new(Period) e := r.Scan(1) g.Expect(e).To(HaveOccurred()) g.Expect(e.Error()).To(ContainSubstring("not a meaningful period")) } golang-github-rickb777-date-1.15.3/rep.go000066400000000000000000000023741410771016400177240ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package date import "time" const secondsPerDay = 60 * 60 * 24 // encode returns the number of days elapsed from date zero to the date // corresponding to the given Time value. func encode(t time.Time) PeriodOfDays { // Compute the number of seconds elapsed since January 1, 1970 00:00:00 // in the location specified by t and not necessarily UTC. // A Time value is represented internally as an offset from a UTC base // time; because we want to extract a date in the time zone specified // by t rather than in UTC, we need to compensate for the time zone // difference. _, offset := t.Zone() secs := t.Unix() + int64(offset) // Unfortunately operator / rounds towards 0, so negative values // must be handled differently if secs >= 0 { return PeriodOfDays(secs / secondsPerDay) } return -PeriodOfDays((secondsPerDay - 1 - secs) / secondsPerDay) } // decode returns the Time value corresponding to 00:00:00 UTC of the date // represented by d, the number of days elapsed since date zero. func decode(d PeriodOfDays) time.Time { secs := int64(d) * secondsPerDay return time.Unix(secs, 0).UTC() } golang-github-rickb777-date-1.15.3/rep_test.go000066400000000000000000000061321410771016400207570ustar00rootroot00000000000000// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package date import ( "math/rand" "testing" "time" ) func TestEncode(t *testing.T) { cases := []int{ 0, 1, 28, 30, 31, 32, 364, 365, 366, 367, 500, 1000, 10000, 100000, } tBase := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) for i, c := range cases { d := encode(tBase.AddDate(0, 0, c)) if d != PeriodOfDays(c) { t.Errorf("Encode(%v) == %v, want %v", i, d, c) } d = encode(tBase.AddDate(0, 0, -c)) if d != PeriodOfDays(-c) { t.Errorf("Encode(%v) == %v, want %v", i, d, c) } } } func TestEncodeDecode(t *testing.T) { cases := []struct { year int month time.Month day int }{ {1969, time.December, 31}, {1970, time.January, 1}, {1970, time.January, 2}, {2000, time.February, 28}, {2000, time.February, 29}, {2000, time.March, 1}, {2004, time.February, 28}, {2004, time.February, 29}, {2004, time.March, 1}, {2100, time.February, 28}, {2100, time.February, 29}, {2100, time.March, 1}, {0, time.January, 1}, {1, time.February, 3}, {19, time.March, 4}, {100, time.April, 5}, {2000, time.May, 6}, {30000, time.June, 7}, {400000, time.July, 8}, {5000000, time.August, 9}, {-1, time.September, 11}, {-19, time.October, 12}, {-100, time.November, 13}, {-2000, time.December, 14}, {-30000, time.February, 15}, {-400000, time.May, 16}, {-5000000, time.September, 17}, } for _, c := range cases { tIn := time.Date(c.year, c.month, c.day, 0, 0, 0, 0, time.UTC) d := encode(tIn) tOut := decode(d) if !tIn.Equal(tOut) { t.Errorf("EncodeDecode(%v) == %v, want %v", c, tOut, tIn) } } } func TestDecodeEncode(t *testing.T) { for i := 0; i < 1000; i++ { c := PeriodOfDays(rand.Int31()) d := encode(decode(c)) if d != c { t.Errorf("DecodeEncode(%v) == %v, want %v", i, d, c) } } for i := 0; i < 1000; i++ { c := -PeriodOfDays(rand.Int31()) d := encode(decode(c)) if d != c { t.Errorf("DecodeEncode(%v) == %v, want %v", i, d, c) } } } // TestZone checks that the conversions between a time.Time value and the // internal representation of a Date value correctly handle time zones other // than UTC, especially in cases where the local date at a given time is // different from the UTC date for that same time. func TestZone(t *testing.T) { cases := []string{ "2015-07-29 15:12:34 +0000", "2015-07-29 15:12:34 -0500", "2015-07-29 15:12:34 +0500", "2015-07-29 21:12:34 -0500", "2015-07-29 21:12:34 -0500", "2015-07-29 03:12:34 +0500", "2015-07-29 03:12:34 +0500", } for _, c := range cases { tIn, err := time.Parse("2006-01-02 15:04:05 -0700", c) if err != nil { t.Errorf("Zone(%v) cannot parse %v", c, c) } d := encode(tIn) tOut := decode(d) yIn, mIn, dIn := tIn.Date() yOut, mOut, dOut := tOut.Date() if yIn != yOut { t.Errorf("Zone(%v).y == %v, want %v", c, yOut, yIn) } if mIn != mOut { t.Errorf("Zone(%v).m == %v, want %v", c, mOut, mIn) } if dIn != dOut { t.Errorf("Zone(%v).d == %v, want %v", c, dOut, dIn) } } } golang-github-rickb777-date-1.15.3/sql.go000066400000000000000000000055701410771016400177360ustar00rootroot00000000000000// 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 date import ( "database/sql/driver" "fmt" "strconv" "time" ) // These methods allow Date and PeriodOfDays to be fields stored in an // SQL database by implementing the database/sql/driver interfaces. // The underlying column type can be an integer (period of days since the epoch), // a string, or a DATE. // Scan parses some value. It implements sql.Scanner, // https://golang.org/pkg/database/sql/#Scanner func (d *Date) Scan(value interface{}) (err error) { if value == nil { return nil } return d.scanAny(value) } func (d *Date) scanAny(value interface{}) (err error) { err = nil switch v := value.(type) { case int64: *d = Date{PeriodOfDays(v)} case []byte: return d.scanString(string(v)) case string: return d.scanString(v) case time.Time: *d = NewAt(v) default: err = fmt.Errorf("%T %+v is not a meaningful date", value, value) } return err } func (d *Date) scanString(value string) (err error) { n, err := strconv.ParseInt(value, 10, 64) if err == nil { *d = Date{PeriodOfDays(n)} return nil } *d, err = AutoParse(value) return err } // Value converts the value to an int64. It implements driver.Valuer, // https://golang.org/pkg/database/sql/driver/#Valuer func (d Date) Value() (driver.Value, error) { return int64(d.day), nil } //------------------------------------------------------------------------------------------------- // DateString alters Date to make database storage use a string column, or // a similar derived column such as SQL DATE. (Otherwise, Date is stored as // an integer). type DateString Date // Date provides a simple fluent type conversion to the underlying type. func (ds DateString) Date() Date { return Date(ds) } // DateString provides a simple fluent type conversion from the underlying type. func (d Date) DateString() DateString { return DateString(d) } // Scan parses some value. It implements sql.Scanner, // https://golang.org/pkg/database/sql/#Scanner func (ds *DateString) Scan(value interface{}) (err error) { if value == nil { return nil } return (*Date)(ds).Scan(value) } // Value converts the value to an int64. It implements driver.Valuer, // https://golang.org/pkg/database/sql/driver/#Valuer func (ds DateString) Value() (driver.Value, error) { return ds.Date().String(), nil } //------------------------------------------------------------------------------------------------- // DisableTextStorage reduces the Scan method so that only integers are handled. // Normally, database types int64, []byte, string and time.Time are supported. // When set true, only int64 is supported; this mode allows optimisation of SQL // result processing and would only be used during development. // // Deprecated: this is no longer used. var DisableTextStorage = false golang-github-rickb777-date-1.15.3/sql_test.go000066400000000000000000000055221410771016400207720ustar00rootroot00000000000000// 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 date import ( "database/sql/driver" "testing" ) func TestDateScan(t *testing.T) { cases := []struct { v interface{} expected PeriodOfDays }{ {int64(0), 0}, {int64(1000), 1000}, {int64(10000), 10000}, {int64(0), 0}, {int64(1000), 1000}, {int64(10000), 10000}, {"0", 0}, {"1000", 1000}, {"10000", 10000}, {"2018-12-31", 17896}, {"31/12/2018", 17896}, {[]byte("10000"), 10000}, {PeriodOfDays(10000).Date().Local(), 10000}, } for i, c := range cases { r := new(Date) e := r.Scan(c.v) if e != nil { t.Errorf("%d: Got %v for %d", i, e, c.expected) } if r.DaysSinceEpoch() != c.expected { t.Errorf("%d: Got %v, want %d", i, *r, c.expected) } var d driver.Valuer = *r q, e := d.Value() if e != nil { t.Errorf("%d: Got %v for %d", i, e, c.expected) } if q.(int64) != int64(c.expected) { t.Errorf("%d: Got %v, want %d", i, q, c.expected) } } } func TestDateStringScan(t *testing.T) { cases := []struct { v interface{} expected string }{ {int64(0), "1970-01-01"}, {int64(15000), "2011-01-26"}, {"0", "1970-01-01"}, {"15000", "2011-01-26"}, {"2018-12-31", "2018-12-31"}, {"31/12/2018", "2018-12-31"}, //{[]byte("10000"), ""}, //{PeriodOfDays(10000).Date().Local(), ""}, } for i, c := range cases { r := new(DateString) e := r.Scan(c.v) if e != nil { t.Errorf("%d: Got %v for %s", i, e, c.expected) } if r.Date().String() != c.expected { t.Errorf("%d: Got %v, want %s", i, r.Date(), c.expected) } var d driver.Valuer = *r q, e := d.Value() if e != nil { t.Errorf("%d: Got %v for %s", i, e, c.expected) } if q.(string) != c.expected { t.Errorf("%d: Got %v, want %s", i, q, c.expected) } } } func TestDateScanWithJunk(t *testing.T) { cases := []struct { v interface{} expected string }{ {true, "bool true is not a meaningful date"}, {true, "bool true is not a meaningful date"}, } for i, c := range cases { r := new(Date) e := r.Scan(c.v) if e.Error() != c.expected { t.Errorf("%d: Got %q, want %q", i, e.Error(), c.expected) } } } func TestDateStringScanWithJunk(t *testing.T) { cases := []struct { v interface{} expected string }{ {true, "bool true is not a meaningful date"}, {true, "bool true is not a meaningful date"}, } for i, c := range cases { r := new(DateString) e := r.Scan(c.v) if e.Error() != c.expected { t.Errorf("%d: Got %q, want %q", i, e.Error(), c.expected) } } } func TestDateScanWithNil(t *testing.T) { var r *Date e := r.Scan(nil) if e != nil { t.Errorf("Got %v", e) } } func TestDateAsStringScanWithNil(t *testing.T) { var r *Date e := r.Scan(nil) if e != nil { t.Errorf("Got %v", e) } } golang-github-rickb777-date-1.15.3/timespan/000077500000000000000000000000001410771016400204215ustar00rootroot00000000000000golang-github-rickb777-date-1.15.3/timespan/daterange.go000066400000000000000000000270661410771016400227150ustar00rootroot00000000000000// 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} } golang-github-rickb777-date-1.15.3/timespan/daterange_test.go000066400000000000000000000255751410771016400237570ustar00rootroot00000000000000// 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" "strings" "testing" "time" . "github.com/rickb777/date" "github.com/rickb777/date/period" ) var d0320 = New(2015, time.March, 20) var d0321 = New(2015, time.March, 21) var d0325 = New(2015, time.March, 25) var d0326 = New(2015, time.March, 26) var d0327 = New(2015, time.March, 27) var d0328 = New(2015, time.March, 28) var d0329 = New(2015, time.March, 29) // n.b. clocks go forward (UK) var d0330 = New(2015, time.March, 30) var d0331 = New(2015, time.March, 31) var d0401 = New(2015, time.April, 1) var d0402 = New(2015, time.April, 2) var d0403 = New(2015, time.April, 3) var d0404 = New(2015, time.April, 4) var d0407 = New(2015, time.April, 7) var d0408 = New(2015, time.April, 8) var d0409 = New(2015, time.April, 9) var d0410 = New(2015, time.April, 10) var d0501 = New(2015, time.May, 1) var d1025 = New(2015, time.October, 25) var london *time.Location = mustLoadLocation("Europe/London") func mustLoadLocation(name string) *time.Location { loc, err := time.LoadLocation("Europe/London") if err != nil { panic(err) } return loc } func TestNewDateRangeOf(t *testing.T) { dr := NewDateRangeOf(t0327, 7*24*time.Hour) isEq(t, 0, dr.mark, d0327) isEq(t, 0, dr.Days(), PeriodOfDays(7)) isEq(t, 0, dr.IsEmpty(), false) isEq(t, 0, dr.Start(), d0327) isEq(t, 0, dr.Last(), d0402) isEq(t, 0, dr.End(), d0403) dr2 := NewDateRangeOf(t0327, -7*24*time.Hour) isEq(t, 0, dr2.mark, d0327) isEq(t, 0, dr2.Days(), PeriodOfDays(7)) isEq(t, 0, dr2.IsEmpty(), false) isEq(t, 0, dr2.Start(), d0321) isEq(t, 0, dr2.Last(), d0327) isEq(t, 0, dr2.End(), d0328) } func TestNewDateRangeWithNormalise(t *testing.T) { r1 := NewDateRange(d0327, d0402) isEq(t, 0, r1.Start(), d0327) isEq(t, 0, r1.Last(), d0401) isEq(t, 0, r1.End(), d0402) r2 := NewDateRange(d0402, d0327) isEq(t, 0, r2.Start(), d0327) isEq(t, 0, r2.Last(), d0401) isEq(t, 0, r2.End(), d0402) } func TestEmptyRange(t *testing.T) { drN0 := DateRange{d0327, -1} isEq(t, 0, drN0.Days(), PeriodOfDays(1)) isEq(t, 0, drN0.IsZero(), false) isEq(t, 0, drN0.IsEmpty(), false) isEq(t, 0, drN0.Start(), d0327) isEq(t, 0, drN0.Last(), d0327) isEq(t, 0, drN0.String(), "1 day on 2015-03-26") dr0 := DateRange{} isEq(t, 0, dr0.Days(), PeriodOfDays(0)) isEq(t, 0, dr0.IsZero(), true) isEq(t, 0, dr0.IsEmpty(), true) isEq(t, 0, dr0.String(), "0 days at 1970-01-01") dr1 := EmptyRange(Date{}) isEq(t, 0, dr1.IsZero(), true) isEq(t, 0, dr1.IsEmpty(), true) isEq(t, 0, dr1.Days(), PeriodOfDays(0)) dr2 := EmptyRange(d0327) isEq(t, 0, dr2.IsZero(), false) isEq(t, 0, dr2.IsEmpty(), true) isEq(t, 0, dr2.Start(), d0327) isEq(t, 0, dr2.Last().IsZero(), true) isEq(t, 0, dr2.End(), d0327) isEq(t, 0, dr2.Days(), PeriodOfDays(0)) isEq(t, 0, dr2.String(), "0 days at 2015-03-27") } func TestOneDayRange(t *testing.T) { dr1 := OneDayRange(Date{}) isEq(t, 0, dr1.IsZero(), false) isEq(t, 0, dr1.IsEmpty(), false) isEq(t, 0, dr1.Days(), PeriodOfDays(1)) dr2 := OneDayRange(d0327) isEq(t, 0, dr2.Start(), d0327) isEq(t, 0, dr2.Last(), d0327) isEq(t, 0, dr2.End(), d0328) isEq(t, 0, dr2.Days(), PeriodOfDays(1)) isEq(t, 0, dr2.String(), "1 day on 2015-03-27") } func TestDayRange(t *testing.T) { dr1 := DayRange(Date{}, 0) isEq(t, 0, dr1.IsZero(), true) isEq(t, 0, dr1.IsEmpty(), true) isEq(t, 0, dr1.Days(), PeriodOfDays(0)) dr2 := DayRange(d0327, 2) isEq(t, 0, dr2.Start(), d0327) isEq(t, 0, dr2.Last(), d0328) isEq(t, 0, dr2.End(), d0329) isEq(t, 0, dr2.Days(), PeriodOfDays(2)) isEq(t, 0, dr2.String(), "2 days from 2015-03-27 to 2015-03-28") dr3 := DayRange(d0327, -2) isEq(t, 0, dr3.Start(), d0325) isEq(t, 0, dr3.Last(), d0326) isEq(t, 0, dr3.End(), d0327) isEq(t, 0, dr3.Days(), PeriodOfDays(2)) isEq(t, 0, dr3.String(), "2 days from 2015-03-25 to 2015-03-26") } func TestNewYearOf(t *testing.T) { dr := NewYearOf(2015) isEq(t, 0, dr.Days(), PeriodOfDays(365)) isEq(t, 0, dr.Start(), New(2015, time.January, 1)) isEq(t, 0, dr.Last(), New(2015, time.December, 31)) isEq(t, 0, dr.End(), New(2016, time.January, 1)) } func TestNewMonthOf(t *testing.T) { dr := NewMonthOf(2015, time.February) isEq(t, 0, dr.Days(), PeriodOfDays(28)) isEq(t, 0, dr.Start(), New(2015, time.February, 1)) isEq(t, 0, dr.Last(), New(2015, time.February, 28)) isEq(t, 0, dr.End(), New(2015, time.March, 1)) } func TestShiftAndExtend(t *testing.T) { cases := []struct { dr DateRange n PeriodOfDays start Date end Date s string }{ {DayRange(d0327, 6).ShiftBy(0), 6, d0327, d0402, "6 days from 2015-03-27 to 2015-04-01"}, {DayRange(d0327, 6).ShiftBy(7), 6, d0403, d0409, "6 days from 2015-04-03 to 2015-04-08"}, {DayRange(d0327, 6).ShiftBy(-1), 6, d0326, d0401, "6 days from 2015-03-26 to 2015-03-31"}, {DayRange(d0327, 6).ShiftBy(-7), 6, d0320, d0326, "6 days from 2015-03-20 to 2015-03-25"}, {NewDateRange(d0327, d0402).ShiftBy(-7), 6, d0320, d0326, "6 days from 2015-03-20 to 2015-03-25"}, {EmptyRange(d0327).ExtendBy(0), 0, d0327, d0327, "0 days at 2015-03-27"}, {EmptyRange(d0327).ExtendBy(6), 6, d0327, d0402, "6 days from 2015-03-27 to 2015-04-01"}, {DayRange(d0327, 6).ExtendBy(0), 6, d0327, d0402, "6 days from 2015-03-27 to 2015-04-01"}, {DayRange(d0327, 6).ExtendBy(7), 13, d0327, d0409, "13 days from 2015-03-27 to 2015-04-08"}, {DayRange(d0327, 6).ExtendBy(-6), 0, d0327, d0327, "0 days at 2015-03-27"}, {DayRange(d0327, 6).ExtendBy(-8), 2, d0325, d0327, "2 days from 2015-03-25 to 2015-03-26"}, {DayRange(d0327, 6).ShiftByPeriod(period.NewYMD(0, 0, 0)), 6, d0327, d0402, "6 days from 2015-03-27 to 2015-04-01"}, {DayRange(d0327, 6).ShiftByPeriod(period.NewYMD(0, 0, 7)), 6, d0403, d0409, "6 days from 2015-04-03 to 2015-04-08"}, {DayRange(d0327, 6).ShiftByPeriod(period.NewYMD(0, 0, -7)), 6, d0320, d0326, "6 days from 2015-03-20 to 2015-03-25"}, {DayRange(d0327, 6).ExtendByPeriod(period.NewYMD(0, 0, 0)), 6, d0327, d0402, "6 days from 2015-03-27 to 2015-04-01"}, {DayRange(d0327, 6).ExtendByPeriod(period.NewYMD(0, 0, 7)), 13, d0327, d0409, "13 days from 2015-03-27 to 2015-04-08"}, {DayRange(d0327, 6).ExtendByPeriod(period.NewYMD(0, 0, -5)), 1, d0327, d0328, "1 day on 2015-03-27"}, {DayRange(d0327, 6).ExtendByPeriod(period.NewYMD(0, 0, -6)), 0, d0327, d0327, "0 days at 2015-03-27"}, {DayRange(d0327, 6).ExtendByPeriod(period.NewYMD(0, 0, -7)), 1, d0326, d0327, "1 day on 2015-03-26"}, } for i, c := range cases { isEq(t, i, c.dr.Days(), c.n) isEq(t, i, c.dr.Start(), c.start) isEq(t, i, c.dr.End(), c.end) isEq(t, i, c.dr.String(), c.s) } } func TestContains0(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) dr := EmptyRange(d0326) isEq(t, 0, dr.Contains(d0320), false, dr, d0320) time.Local = old } func TestContains1(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) dr := DayRange(d0326, 2) isEq(t, 0, dr.Contains(d0320), false, dr, d0320) isEq(t, 0, dr.Contains(d0325), false, dr, d0325) isEq(t, 0, dr.Contains(d0326), true, dr, d0326) isEq(t, 0, dr.Contains(d0327), true, dr, d0327) isEq(t, 0, dr.Contains(d0328), false, dr, d0328) isEq(t, 0, dr.Contains(d0401), false, dr, d0401) isEq(t, 0, dr.Contains(d0410), false, dr, d0410) isEq(t, 0, dr.Contains(d0501), false, dr, d0501) time.Local = old } func TestContains2(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) dr := OneDayRange(d0326) isEq(t, 0, dr.Contains(d0325), false, dr, d0325) isEq(t, 0, dr.Contains(d0326), true, dr, d0326) isEq(t, 0, dr.Contains(d0327), false, dr, d0327) time.Local = old } func TestContainsTime0(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) t0328e := time.Date(2015, 3, 28, 23, 59, 59, 999999999, time.UTC) t0329 := time.Date(2015, 3, 29, 0, 0, 0, 0, time.UTC) dr := EmptyRange(d0327) isEq(t, 0, dr.StartUTC(), t0327, dr, t0327) isEq(t, 0, dr.EndUTC(), t0327, dr, t0327) isEq(t, 0, dr.ContainsTime(t0327), false, dr, t0327) isEq(t, 0, dr.ContainsTime(t0328), false, dr, t0328) isEq(t, 0, dr.ContainsTime(t0328e), false, dr, t0328e) isEq(t, 0, dr.ContainsTime(t0329), false, dr, t0329) time.Local = old } func TestContainsTimeUTC(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) t0328e := time.Date(2015, 3, 28, 23, 59, 59, 999999999, time.UTC) t0329 := time.Date(2015, 3, 29, 0, 0, 0, 0, time.UTC) dr := DayRange(d0327, 2) isEq(t, 0, dr.StartUTC(), t0327, dr, t0327) isEq(t, 0, dr.EndUTC(), t0329, dr, t0329) isEq(t, 0, dr.ContainsTime(t0327), true, dr, t0327) isEq(t, 0, dr.ContainsTime(t0328), true, dr, t0328) isEq(t, 0, dr.ContainsTime(t0328e), true, dr, t0328e) isEq(t, 0, dr.ContainsTime(t0329), false, dr, t0329) time.Local = old } func TestMerge1(t *testing.T) { dr1 := DayRange(d0327, 2) dr2 := DayRange(d0327, 8) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, 0, m1.Start(), d0327) isEq(t, 0, m1.End(), d0404) isEq(t, 0, m1, m2) } func TestMerge2(t *testing.T) { dr1 := DayRange(d0328, 2) dr2 := DayRange(d0327, 8) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, 0, m1.Start(), d0327) isEq(t, 0, m1.End(), d0404) isEq(t, 0, m1, m2) } func TestMergeOverlapping(t *testing.T) { dr1 := OneDayRange(d0320).ExtendBy(12) dr2 := OneDayRange(d0401).ExtendBy(6) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, 0, m1.Start(), d0320) isEq(t, 0, m1.End(), d0408) isEq(t, 0, m1, m2) } func TestMergeNonOverlapping(t *testing.T) { dr1 := OneDayRange(d0320).ExtendBy(2) dr2 := OneDayRange(d0401).ExtendBy(6) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, 0, m1.Start(), d0320) isEq(t, 0, m1.End(), d0408) isEq(t, 0, m1, m2) } func TestMergeEmpties(t *testing.T) { dr1 := EmptyRange(d0320) dr2 := EmptyRange(d0408) // curiously, this is *not* included because it has no size. m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, 0, m1.Start(), d0320) isEq(t, 0, m1.End(), d0408) isEq(t, 0, m1, m2) } func TestMergeZeroes(t *testing.T) { dr0 := DateRange{} dr1 := OneDayRange(d0401).ExtendBy(6) m1 := dr1.Merge(dr0) m2 := dr0.Merge(dr1) m3 := dr0.Merge(dr0) isEq(t, 0, m1.Start(), d0401) isEq(t, 0, m1.End(), d0408) isEq(t, 0, m1, m2) isEq(t, 0, m3.IsZero(), true) isEq(t, 0, m3, dr0) } func TestDurationNormalUTC(t *testing.T) { dr := OneDayRange(d0329) isEq(t, 0, dr.Duration(), time.Hour*24) } func TestDurationInZoneWithDaylightSaving(t *testing.T) { isEq(t, 0, OneDayRange(d0328).DurationIn(london), time.Hour*24) isEq(t, 0, OneDayRange(d0329).DurationIn(london), time.Hour*23) isEq(t, 0, OneDayRange(d1025).DurationIn(london), time.Hour*25) isEq(t, 0, NewDateRange(d0328, d0331).DurationIn(london), time.Hour*71) } func isEq(t *testing.T, i int, a, b interface{}, msg ...interface{}) { t.Helper() if a != b { sa := make([]string, len(msg)) for i, m := range msg { sa[i] = fmt.Sprintf(", %v", m) } t.Errorf("%d: %+v is not equal to %+v%s", i, a, b, strings.Join(sa, "")) } } golang-github-rickb777-date-1.15.3/timespan/doc.go000066400000000000000000000006611410771016400215200ustar00rootroot00000000000000// 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 provides spans of time (TimeSpan), and ranges of dates (DateRange). // Both are half-open intervals for which the start is included and the end is excluded. // This allows for empty spans and also facilitates aggregating spans together. // package timespan golang-github-rickb777-date-1.15.3/timespan/timespan.go000066400000000000000000000237211410771016400225750ustar00rootroot00000000000000// 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" "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". const TimestampFormat = "2006-01-02 15:04:05" //const ISOFormat = "2006-01-02T15:04:05" // TimeSpan holds a span of time between two instants with a 1 nanosecond resolution. // It is implemented using a time.Duration, therefore is limited to a maximum span of 290 years. type TimeSpan struct { mark time.Time duration time.Duration } // ZeroTimeSpan creates a new zero-duration time span at a specified time. func ZeroTimeSpan(start time.Time) TimeSpan { return TimeSpan{start, 0} } // TimeSpanOf creates a new time span at a specified time and duration. func TimeSpanOf(start time.Time, d time.Duration) TimeSpan { return TimeSpan{start, d} } // NewTimeSpan creates a new time span from two times. The start and end can be in either // order; the result will be normalised. The inputs are half-open: the start is included and // the end is excluded. func NewTimeSpan(t1, t2 time.Time) TimeSpan { if t2.Before(t1) { return TimeSpan{t2, t1.Sub(t2)} } return TimeSpan{t1, t2.Sub(t1)} } // Start gets the end time of the time span. func (ts TimeSpan) Start() time.Time { if ts.duration < 0 { return ts.mark.Add(ts.duration) } return ts.mark } // End gets the end time of the time span. Strictly, this is one nanosecond after the // range of time included in the time span; this implements the half-open model. func (ts TimeSpan) End() time.Time { if ts.duration < 0 { return ts.mark } return ts.mark.Add(ts.duration) } // Duration gets the duration of the time span. func (ts TimeSpan) Duration() time.Duration { return ts.duration } // IsEmpty returns true if this is an empty time span (zero duration). func (ts TimeSpan) IsEmpty() bool { return ts.duration == 0 } // Normalise ensures that the mark time is at the start time and the duration is positive. // The normalised time span is returned. func (ts TimeSpan) Normalise() TimeSpan { if ts.duration < 0 { return TimeSpan{ts.mark.Add(ts.duration), -ts.duration} } return ts } // ShiftBy moves the time span by moving both the start and end times similarly. // A negative parameter is allowed. func (ts TimeSpan) ShiftBy(d time.Duration) TimeSpan { return TimeSpan{ts.mark.Add(d), ts.duration} } // ExtendBy lengthens the time span by a specified amount. The parameter may be negative, // in which case it is possible that the end of the time span will appear to be before the // start. However, the result is normalised so that the resulting start is the lesser value. func (ts TimeSpan) ExtendBy(d time.Duration) TimeSpan { return TimeSpan{ts.mark, ts.duration + d}.Normalise() } // ExtendWithoutWrapping lengthens the time span by a specified amount. The parameter may be // negative, but if its magnitude is large than the time span's duration, it will be truncated // so that the result has zero duration in that case. The start time is never altered. func (ts TimeSpan) ExtendWithoutWrapping(d time.Duration) TimeSpan { tsn := ts.Normalise() if d < 0 && -d > tsn.duration { return TimeSpan{tsn.mark, 0} } return TimeSpan{tsn.mark, tsn.duration + d} } // String produces a human-readable description of a time span. func (ts TimeSpan) String() string { return fmt.Sprintf("%s from %s to %s", ts.duration, ts.mark.Format(TimestampFormat), ts.End().Format(TimestampFormat)) } // In returns a TimeSpan adjusted from its current location to a new location. Because // location is considered to be a presentational attribute, the actual time itself is not // altered by this function. This matches the behaviour of time.Time.In(loc). func (ts TimeSpan) In(loc *time.Location) TimeSpan { t := ts.mark.In(loc) return TimeSpan{t, ts.duration} } // DateRangeIn obtains the date range corresponding to the time span in a specified location. // The result is normalised. func (ts TimeSpan) DateRangeIn(loc *time.Location) DateRange { no := ts.Normalise() startDate := date.NewAt(no.mark.In(loc)) endDate := date.NewAt(no.End().In(loc)) return NewDateRange(startDate, endDate) } // Contains tests whether a given moment of time is enclosed within the time span. The // start time is inclusive; the end time is exclusive. // If t has a different locality to the time-span, it is adjusted accordingly. func (ts TimeSpan) Contains(t time.Time) bool { tl := t.In(ts.mark.Location()) return ts.mark.Equal(tl) || ts.mark.Before(tl) && ts.End().After(tl) } // Merge combines two time spans by calculating a time span that just encompasses them both. // As a special case, if one span is entirely contained within the other span, the larger of // the two is returned. Otherwise, the result is the start of the earlier one to the end of the // later one, even if the two spans don't overlap. func (ts TimeSpan) Merge(other TimeSpan) TimeSpan { if ts.mark.After(other.mark) { // swap the ranges to simplify the logic return other.Merge(ts) } else if ts.End().After(other.End()) { // other is a proper subrange of ts return ts } else { return NewTimeSpan(ts.mark, other.End()) } } // RFC5545DateTimeLayout is the format string used by iCalendar (RFC5545). Note // that "Z" is to be appended when the time is UTC. const RFC5545DateTimeLayout = "20060102T150405" // RFC5545DateTimeZulu is the UTC format string used by iCalendar (RFC5545). Note // that this cannot be used for parsing with time.Parse. const RFC5545DateTimeZulu = RFC5545DateTimeLayout + "Z" func layoutHasTimezone(layout string) bool { return strings.IndexByte(layout, 'Z') >= 0 || strings.Contains(layout, "-07") } // Equal reports whether ts and us represent the same time start and duration. // Two times can be equal even if they are in different locations. // For example, 6:00 +0200 CEST and 4:00 UTC are Equal. func (ts TimeSpan) Equal(us TimeSpan) bool { return ts.Duration() == us.Duration() && ts.Start().Equal(us.Start()) } // Format returns a textual representation of the time value formatted according to layout. // It produces a string containing the start and end time. Or, if useDuration is true, // it returns a string containing the start time and the duration. // // The layout string is as specified for time.Format. If it doesn't have a timezone element // ("07" or "Z") and the times in the timespan are UTC, the "Z" zulu indicator is added. // This is as required by iCalendar (RFC5545). // // Also, if the layout is blank, it defaults to RFC5545DateTimeLayout. // // The separator between the two parts of the result would be "/" for RFC5545, but can be // anything. func (ts TimeSpan) Format(layout, separator string, useDuration bool) string { if layout == "" { layout = RFC5545DateTimeLayout } // if the time is UTC and the format doesn't contain zulu field ("Z") or timezone field ("07") if ts.mark.Location().String() == "UTC" && !layoutHasTimezone(layout) { layout = RFC5545DateTimeZulu } s := ts.Start() e := ts.End() if useDuration { p := period.Between(s, e) return fmt.Sprintf("%s%s%s", s.Format(layout), separator, p) } return fmt.Sprintf("%s%s%s", s.Format(layout), separator, e.Format(layout)) } // FormatRFC5545 formats the timespan as a string containing the start time and end time, or the // start time and duration, if useDuration is true. The two parts are separated by slash. // The time(s) is expressed as UTC zulu. // This is as required by iCalendar (RFC5545). func (ts TimeSpan) FormatRFC5545(useDuration bool) string { return ts.Format(RFC5545DateTimeZulu, "/", useDuration) } // MarshalText formats the timespan as a string using, using RFC5545 layout. // This implements the encoding.TextMarshaler interface. func (ts TimeSpan) MarshalText() (text []byte, err error) { s := ts.Format(RFC5545DateTimeZulu, "/", true) return []byte(s), nil } // ParseRFC5545InLocation parses a string as a timespan. The string must contain either of // // time "/" time // time "/" period // // If the input time(s) ends in "Z", the location is UTC (as per RFC5545). Otherwise, the // specified location will be used for the resulting times; this behaves the same as // time.ParseInLocation. func ParseRFC5545InLocation(text string, loc *time.Location) (TimeSpan, error) { slash := strings.IndexByte(text, '/') if slash < 0 { return TimeSpan{}, fmt.Errorf("cannot parse %q because there is no separator '/'", text) } start := text[:slash] rest := text[slash+1:] st, err := parseTimeInLocation(start, loc) if err != nil { return TimeSpan{}, fmt.Errorf("cannot parse start time in %q: %s", text, err.Error()) } //fmt.Printf("got %20s %s\n", st.Location(), st.Format(RFC5545DateTimeLayout)) if rest == "" { return TimeSpan{}, fmt.Errorf("cannot parse %q because there is end time or duration", text) } if rest[0] == 'P' { pe, e2 := period.Parse(rest) if e2 != nil { return TimeSpan{}, fmt.Errorf("cannot parse period in %q: %s", text, e2.Error()) } du, precise := pe.Duration() if precise { return TimeSpan{st, du}, nil } et := st.AddDate(pe.Years(), pe.Months(), pe.Days()) return NewTimeSpan(st, et), nil } et, err := parseTimeInLocation(rest, loc) return NewTimeSpan(st, et), err } func parseTimeInLocation(text string, loc *time.Location) (time.Time, error) { if strings.HasSuffix(text, "Z") { text = text[:len(text)-1] return time.ParseInLocation(RFC5545DateTimeLayout, text, time.UTC) } return time.ParseInLocation(RFC5545DateTimeLayout, text, loc) } // UnmarshalText parses a string as a timespan. It expects RFC5545 layout. // // If the receiver timespan is non-nil and has a time with a location, // this location is used for parsing. Otherwise time.Local is used. // // This implements the encoding.TextUnmarshaler interface. func (ts *TimeSpan) UnmarshalText(text []byte) (err error) { loc := time.Local if ts != nil { loc = ts.mark.Location() } *ts, err = ParseRFC5545InLocation(string(text), loc) return } golang-github-rickb777-date-1.15.3/timespan/timespan_test.go000066400000000000000000000244531410771016400236370ustar00rootroot00000000000000// 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 ( "testing" "time" "github.com/rickb777/date" ) const zero time.Duration = 0 var t0327 = time.Date(2015, 3, 27, 0, 0, 0, 0, time.UTC) var t0328 = time.Date(2015, 3, 28, 0, 0, 0, 0, time.UTC) var t0329 = time.Date(2015, 3, 29, 0, 0, 0, 0, time.UTC) // n.b. clocks go forward (UK) var t0330 = time.Date(2015, 3, 30, 0, 0, 0, 0, time.UTC) func TestZeroTimeSpan(t *testing.T) { ts := ZeroTimeSpan(t0327) isEq(t, 0, ts.mark, t0327) isEq(t, 0, ts.Duration(), zero) isEq(t, 0, ts.End(), t0327) } func TestNewTimeSpan(t *testing.T) { ts1 := NewTimeSpan(t0327, t0327) isEq(t, 0, ts1.mark, t0327) isEq(t, 0, ts1.Duration(), zero) isEq(t, 0, ts1.IsEmpty(), true) isEq(t, 0, ts1.End(), t0327) ts2 := NewTimeSpan(t0327, t0328) isEq(t, 0, ts2.mark, t0327) isEq(t, 0, ts2.Duration(), time.Hour*24) isEq(t, 0, ts2.IsEmpty(), false) isEq(t, 0, ts2.End(), t0328) ts3 := NewTimeSpan(t0329, t0327) isEq(t, 0, ts3.mark, t0327) isEq(t, 0, ts3.Duration(), time.Hour*48) isEq(t, 0, ts3.IsEmpty(), false) isEq(t, 0, ts3.End(), t0329) } func TestTSEnd(t *testing.T) { ts1 := TimeSpan{t0328, time.Hour * 24} isEq(t, 0, ts1.Start(), t0328) isEq(t, 0, ts1.End(), t0329) // not normalised, deliberately ts2 := TimeSpan{t0328, -time.Hour * 24} isEq(t, 0, ts2.Start(), t0327) isEq(t, 0, ts2.End(), t0328) } func TestTSShiftBy(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ShiftBy(time.Hour * 24) isEq(t, 0, ts1.mark, t0328) isEq(t, 0, ts1.Duration(), time.Hour*24) isEq(t, 0, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ShiftBy(-time.Hour * 24) isEq(t, 0, ts2.mark, t0327) isEq(t, 0, ts2.Duration(), time.Hour*24) isEq(t, 0, ts2.End(), t0328) } func TestTSExtendBy(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ExtendBy(time.Hour * 24) isEq(t, 0, ts1.mark, t0327) isEq(t, 0, ts1.Duration(), time.Hour*48) isEq(t, 0, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ExtendBy(-time.Hour * 48) isEq(t, 0, ts2.mark, t0327) isEq(t, 0, ts2.Duration(), time.Hour*24) isEq(t, 0, ts2.End(), t0328) } func TestTSExtendWithoutWrapping(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ExtendWithoutWrapping(time.Hour * 24) isEq(t, 0, ts1.mark, t0327) isEq(t, 0, ts1.Duration(), time.Hour*48) isEq(t, 0, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ExtendWithoutWrapping(-time.Hour * 48) isEq(t, 0, ts2.mark, t0328) isEq(t, 0, ts2.Duration(), zero) isEq(t, 0, ts2.End(), t0328) } func TestTSString(t *testing.T) { s := NewTimeSpan(t0327, t0328).String() isEq(t, 0, s, "24h0m0s from 2015-03-27 00:00:00 to 2015-03-28 00:00:00") } func TestTSEqual(t *testing.T) { // use Berlin, which is UTC+1/+2 berlin, _ := time.LoadLocation("Europe/Berlin") t0 := time.Date(2015, 2, 20, 10, 13, 25, 0, time.UTC) t1 := t0.Add(time.Hour) z0 := ZeroTimeSpan(t0) ts1 := z0.ExtendBy(time.Hour) cases := []struct { a, b TimeSpan }{ {z0, NewTimeSpan(t0, t0)}, {z0, z0.In(berlin)}, {ts1, ts1}, {ts1, NewTimeSpan(t0, t1)}, {ts1, ts1.In(berlin)}, {ts1, ZeroTimeSpan(t1).ExtendBy(-time.Hour)}, } for i, c := range cases { if !c.a.Equal(c.b) { t.Errorf("%d: %v is not equal to %v", i, c.a, c.b) } } } func TestTSNotEqual(t *testing.T) { t0 := time.Date(2015, 2, 20, 10, 13, 25, 0, time.UTC) t1 := t0.Add(time.Hour) cases := []struct { a, b TimeSpan }{ {ZeroTimeSpan(t0), TimeSpanOf(t0, time.Hour)}, {ZeroTimeSpan(t0), ZeroTimeSpan(t1)}, } for i, c := range cases { if c.a.Equal(c.b) { t.Errorf("%d: %v is not equal to %v", i, c.a, c.b) } } } func TestTSFormat(t *testing.T) { // use Berlin, which is UTC-1 berlin, _ := time.LoadLocation("Europe/Berlin") t0 := time.Date(2015, 3, 27, 10, 13, 14, 0, time.UTC) cases := []struct { start time.Time duration time.Duration useDuration bool layout, separator, exp string }{ {t0, time.Hour, true, "", " for ", "20150327T101314Z for PT1H"}, {t0, time.Hour, true, "", "/", "20150327T101314Z/PT1H"}, {t0.In(berlin), time.Minute, true, "", "/", "20150327T111314/PT1M"}, {t0.In(berlin), time.Hour, true, "2006-01-02T15:04:05", "/", "2015-03-27T11:13:14/PT1H"}, {t0.In(berlin), time.Hour, true, "2006-01-02T15:04:05-07", "/", "2015-03-27T11:13:14+01/PT1H"}, {t0, time.Hour, true, "2006-01-02T15:04:05-07", "/", "2015-03-27T10:13:14+00/PT1H"}, {t0, time.Hour, true, "2006-01-02T15:04:05Z07", "/", "2015-03-27T10:13:14Z/PT1H"}, {t0, time.Hour, false, "", " to ", "20150327T101314Z to 20150327T111314Z"}, {t0, time.Hour, false, "", "/", "20150327T101314Z/20150327T111314Z"}, {t0.In(berlin), time.Minute, false, "", "/", "20150327T111314/20150327T111414"}, {t0.In(berlin), time.Hour, false, "2006-01-02T15:04:05", "/", "2015-03-27T11:13:14/2015-03-27T12:13:14"}, {t0.In(berlin), time.Hour, false, "2006-01-02T15:04:05-07", "/", "2015-03-27T11:13:14+01/2015-03-27T12:13:14+01"}, {t0, time.Hour, false, "2006-01-02T15:04:05-07", "/", "2015-03-27T10:13:14+00/2015-03-27T11:13:14+00"}, {t0, time.Hour, false, "2006-01-02T15:04:05Z07", "/", "2015-03-27T10:13:14Z/2015-03-27T11:13:14Z"}, } for _, c := range cases { ts := TimeSpan{c.start, c.duration} isEq(t, 0, ts.Format(c.layout, c.separator, c.useDuration), c.exp) } } func TestTSMarshalText(t *testing.T) { // use Berlin, which is UTC+1 or +2 in summer berlin, _ := time.LoadLocation("Europe/Berlin") t0 := time.Date(2015, 2, 14, 10, 13, 14, 0, time.UTC) t1 := time.Date(2015, 6, 27, 10, 13, 15, 0, time.UTC) cases := []struct { start time.Time duration time.Duration exp string }{ {t0, time.Hour, "20150214T101314Z/PT1H"}, {t1, 2 * time.Hour, "20150627T101315Z/PT2H"}, {t0.In(berlin), time.Minute, "20150214T111314Z/PT1M"}, // UTC+1 {t1.In(berlin), time.Second, "20150627T121315Z/PT1S"}, // UTC+2 } for i, c := range cases { ts := TimeSpan{c.start, c.duration} s := ts.FormatRFC5545(true) isEq(t, i, s, c.exp) b, err := ts.MarshalText() isEq(t, i, err, nil) isEq(t, i, string(b), c.exp) } } func TestTSParseInLocation(t *testing.T) { // use Berlin, which is UTC-1 berlin, _ := time.LoadLocation("Europe/Berlin") t0120 := time.Date(2015, 1, 20, 10, 13, 14, 0, time.UTC) // just before start of daylight savings t0325 := time.Date(2015, 3, 25, 10, 13, 14, 0, time.UTC) cases := []struct { start time.Time duration time.Duration text string }{ {t0325, time.Hour, "20150325T101314Z/PT1H"}, {t0325, 2 * time.Second, "20150325T101314Z/PT2S"}, {t0120.In(berlin), time.Minute, "20150120T111314/PT1M"}, {t0325, 336 * time.Hour, "20150325T101314Z/P2W"}, {t0120.In(berlin), 72 * time.Hour, "20150120T111314/P3D"}, // This case has the daylight-savings clock shift {t0325.In(berlin), 167 * time.Hour, "20150325T111314/P1W"}, } for i, c := range cases { ts1, err := ParseRFC5545InLocation(c.text, c.start.Location()) if err != nil { t.Errorf("%d: %s %v %v", i, c.text, ts1.String(), err) } if !ts1.Start().Equal(c.start) { t.Errorf("%d: %s", i, ts1) } if ts1.Duration() != c.duration { t.Errorf("%d: %s", i, ts1) } ts2 := TimeSpan{}.In(c.start.Location()) err = ts2.UnmarshalText([]byte(c.text)) if err != nil { t.Errorf("%d: %s: %v %v", i, c.text, ts2.String(), err) } if !ts1.Equal(ts2) { t.Errorf("%d: %s: %v is not equal to %v", i, c.text, ts1, ts2) } } } func TestTSParseInLocationErrors(t *testing.T) { cases := []struct { text string }{ {"20150327T101314Z PT1H"}, {"2015XX27T101314/PT1H"}, {"20150127T101314/2016XX27T101314"}, {"20150127T101314/P1Z"}, {"20150327T101314Z/"}, {"/PT1H"}, } for _, c := range cases { ts, err := ParseRFC5545InLocation(c.text, time.UTC) if err == nil { t.Errorf(ts.String()) } } } func TestTSContains(t *testing.T) { ts := NewTimeSpan(t0327, t0329) isEq(t, 0, ts.Contains(t0327.Add(minusOneNano)), false) isEq(t, 0, ts.Contains(t0327), true) isEq(t, 0, ts.Contains(t0328), true) isEq(t, 0, ts.Contains(t0329.Add(minusOneNano)), true) isEq(t, 0, ts.Contains(t0329), false) } func TestTSIn(t *testing.T) { ts := ZeroTimeSpan(t0327).In(time.FixedZone("Test", 7200)) isEq(t, 0, ts.mark.Equal(t0327), true) isEq(t, 0, ts.Duration(), zero) isEq(t, 0, ts.End().Equal(t0327), true) } func TestTSMerge1(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328) ts2 := NewTimeSpan(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) isEq(t, 0, m1.mark, t0327) isEq(t, 0, m1.End(), t0330) isEq(t, 0, m1, m2) } func TestTSMerge2(t *testing.T) { ts1 := NewTimeSpan(t0328, t0329) ts2 := NewTimeSpan(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) isEq(t, 0, m1.mark, t0327) isEq(t, 0, m1.End(), t0330) isEq(t, 0, m1, m2) } func TestTSMerge3(t *testing.T) { ts1 := NewTimeSpan(t0329, t0330) ts2 := NewTimeSpan(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) isEq(t, 0, m1.mark, t0327) isEq(t, 0, m1.End(), t0330) isEq(t, 0, m1, m2) } func TestTSMergeOverlapping(t *testing.T) { ts1 := NewTimeSpan(t0327, t0329) ts2 := NewTimeSpan(t0328, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) isEq(t, 0, m1.mark, t0327) isEq(t, 0, m1.End(), t0330) isEq(t, 0, m1, m2) } func TestTSMergeNonOverlapping(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328) ts2 := NewTimeSpan(t0329, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) isEq(t, 0, m1.mark, t0327) isEq(t, 0, m1.End(), t0330) isEq(t, 0, m1, m2) } func TestConversion1(t *testing.T) { ts1 := ZeroTimeSpan(t0327) dr := ts1.DateRangeIn(time.UTC) ts2 := dr.TimeSpanIn(time.UTC) isEq(t, 0, dr.Start(), d0327) isEq(t, 0, dr.IsEmpty(), true) isEq(t, 0, ts1.Start(), ts1.End()) isEq(t, 0, ts1.Duration(), zero) isEq(t, 0, dr.Days(), date.PeriodOfDays(0)) isEq(t, 0, ts2.Duration(), zero) isEq(t, 0, ts1, ts2) } func TestConversion2(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328) dr := ts1.DateRangeIn(time.UTC) ts2 := dr.TimeSpanIn(time.UTC) isEq(t, 0, dr.Start(), d0327) isEq(t, 0, dr.End(), d0328) isEq(t, 0, ts1, ts2) isEq(t, 0, ts1.Duration(), time.Hour*24) } func TestConversion3(t *testing.T) { dr1 := NewDateRange(d0327, d0330) // weekend of clocks changing ts1 := dr1.TimeSpanIn(london) dr2 := ts1.DateRangeIn(london) ts2 := dr2.TimeSpanIn(london) isEq(t, 0, dr1.Start(), d0327) isEq(t, 0, dr1.End(), d0330) isEq(t, 0, dr1, dr2) isEq(t, 0, ts1, ts2) isEq(t, 0, ts1.Duration(), time.Hour*71) } golang-github-rickb777-date-1.15.3/view/000077500000000000000000000000001410771016400175535ustar00rootroot00000000000000golang-github-rickb777-date-1.15.3/view/vdate.go000066400000000000000000000116671410771016400212200ustar00rootroot00000000000000// 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 view provides a simple API for formatting dates as strings in a manner that is easy to use in view-models, // especially when using Go templates. package view import ( "github.com/rickb777/date" ) const ( // DMYFormat is a typical British representation. DMYFormat = "02/01/2006" // MDYFormat is a typical American representation. MDYFormat = "01/02/2006" // ISOFormat is ISO-8601 YYYY-MM-DD. ISOFormat = "2006-02-01" // DefaultFormat is used by Format() unless a different format is set. DefaultFormat = DMYFormat ) // A VDate holds a Date and provides easy ways to render it, e.g. in Go templates. type VDate struct { d date.Date f string } // NewVDate wraps a Date. func NewVDate(d date.Date) VDate { return VDate{d, DefaultFormat} } // Date returns the underlying date. func (v VDate) Date() date.Date { return v.d } // IsYesterday returns true if the date is yesterday's date. func (v VDate) IsYesterday() bool { return v.d.DaysSinceEpoch()+1 == date.Today().DaysSinceEpoch() } // IsToday returns true if the date is today's date. func (v VDate) IsToday() bool { return v.d.DaysSinceEpoch() == date.Today().DaysSinceEpoch() } // IsTomorrow returns true if the date is tomorrow's date. func (v VDate) IsTomorrow() bool { return v.d.DaysSinceEpoch()-1 == date.Today().DaysSinceEpoch() } // IsOdd returns true if the date is an odd number. This is useful for // zebra striping etc. func (v VDate) IsOdd() bool { return v.d.DaysSinceEpoch()%2 == 0 } // String formats the date in basic ISO8601 format YYYY-MM-DD. func (v VDate) String() string { if v.d.IsZero() { return "" } return v.d.String() } // WithFormat creates a new instance containing the specified format string. func (v VDate) WithFormat(f string) VDate { return VDate{v.d, f} } // Format formats the date using the specified format string, or "02/01/2006" by default. // Use WithFormat to set this up. func (v VDate) Format() string { return v.d.Format(v.f) } // Mon returns the day name as three letters. func (v VDate) Mon() string { return v.d.Format("Mon") } // Monday returns the full day name. func (v VDate) Monday() string { return v.d.Format("Monday") } // Day2 returns the day number without a leading zero. func (v VDate) Day2() string { return v.d.Format("2") } // Day02 returns the day number with a leading zero if necessary. func (v VDate) Day02() string { return v.d.Format("02") } // Day2nd returns the day number without a leading zero but with the appropriate // "st", "nd", "rd", "th" suffix. func (v VDate) Day2nd() string { return v.d.Format("2nd") } // Month1 returns the month number without a leading zero. func (v VDate) Month1() string { return v.d.Format("1") } // Month01 returns the month number with a leading zero if necessary. func (v VDate) Month01() string { return v.d.Format("01") } // Jan returns the month name abbreviated to three letters. func (v VDate) Jan() string { return v.d.Format("Jan") } // January returns the full month name. func (v VDate) January() string { return v.d.Format("January") } // Year returns the four-digit year. func (v VDate) Year() string { return v.d.Format("2006") } // Next returns a fluent generator for later dates. func (v VDate) Next() VDateDelta { return VDateDelta{v.d, v.f, 1} } // Previous returns a fluent generator for earlier dates. func (v VDate) Previous() VDateDelta { return VDateDelta{v.d, v.f, -1} } //------------------------------------------------------------------------------------------------- // Only lossy transcoding is supported here because the intention is that data exchange should be // via the main Date type; VDate is only intended for output through view layers. // MarshalText implements the encoding.TextMarshaler interface. func (v VDate) MarshalText() ([]byte, error) { return v.d.MarshalText() } // UnmarshalText implements the encoding.TextUnmarshaler interface. // Note that the format value gets lost. func (v *VDate) UnmarshalText(data []byte) (err error) { u := &date.Date{} err = u.UnmarshalText(data) if err == nil { v.d = *u v.f = DefaultFormat } return err } //------------------------------------------------------------------------------------------------- // VDateDelta is a VDate with the ability to add or subtract days, weeks, months or years. type VDateDelta struct { d date.Date f string sign date.PeriodOfDays } // Day adds or subtracts one day. func (dd VDateDelta) Day() VDate { return VDate{dd.d.Add(dd.sign), dd.f} } // Week adds or subtracts one week. func (dd VDateDelta) Week() VDate { return VDate{dd.d.Add(dd.sign * 7), dd.f} } // Month adds or subtracts one month. func (dd VDateDelta) Month() VDate { return VDate{dd.d.AddDate(0, int(dd.sign), 0), dd.f} } // Year adds or subtracts one year. func (dd VDateDelta) Year() VDate { return VDate{dd.d.AddDate(int(dd.sign), 0, 0), dd.f} } golang-github-rickb777-date-1.15.3/view/vdate_test.go000066400000000000000000000112431410771016400222450ustar00rootroot00000000000000// 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 view import ( "encoding/json" "testing" "time" "github.com/rickb777/date" ) func TestBasicFormatting(t *testing.T) { d := NewVDate(date.New(2016, 2, 7)) is(t, d.String(), "2016-02-07") is(t, d.Format(), "07/02/2016") is(t, d.WithFormat(MDYFormat).Format(), "02/07/2016") is(t, d.Mon(), "Sun") is(t, d.Monday(), "Sunday") is(t, d.Day2(), "7") is(t, d.Day02(), "07") is(t, d.Day2nd(), "7th") is(t, d.Month1(), "2") is(t, d.Month01(), "02") is(t, d.Jan(), "Feb") is(t, d.January(), "February") is(t, d.Year(), "2016") } func TestZeroFormatting(t *testing.T) { d := NewVDate(date.Date{}) is(t, d.String(), "") is(t, d.Format(), "01/01/1970") is(t, d.WithFormat(ISOFormat).Format(), "1970-01-01") is(t, d.Mon(), "Thu") is(t, d.Monday(), "Thursday") is(t, d.Day2(), "1") is(t, d.Day02(), "01") is(t, d.Day2nd(), "1st") is(t, d.Month1(), "1") is(t, d.Month01(), "01") is(t, d.Jan(), "Jan") is(t, d.January(), "January") is(t, d.Year(), "1970") } func TestDate(t *testing.T) { d := date.New(2016, 2, 7) vd := NewVDate(d) if vd.Date() != d { t.Errorf("%v != %v", vd.Date(), d) } } func TestIsToday(t *testing.T) { today := date.Today() cases := []struct { value VDate expectYesterday bool expectToday bool expectTomorrow bool }{ {NewVDate(date.New(2012, time.June, 25)), false, false, false}, {NewVDate(today.Add(-2)), false, false, false}, {NewVDate(today.Add(-1)), true, false, false}, {NewVDate(today.Add(0)), false, true, false}, {NewVDate(today.Add(1)), false, false, true}, {NewVDate(today.Add(2)), false, false, false}, } for _, c := range cases { if c.value.IsYesterday() != c.expectYesterday { t.Errorf("%s should be 'yesterday': %v", c.value, c.expectYesterday) } if c.value.IsToday() != c.expectToday { t.Errorf("%s should be 'today': %v", c.value, c.expectToday) } if c.value.IsTomorrow() != c.expectTomorrow { t.Errorf("%s should be 'tomorrow': %v", c.value, c.expectTomorrow) } } } func TestIsOdd(t *testing.T) { d25 := date.New(2012, time.June, 25) cases := []struct { value VDate expectOdd bool }{ {NewVDate(d25), true}, {NewVDate(d25.Add(-2)), true}, {NewVDate(d25.Add(-1)), false}, {NewVDate(d25.Add(0)), true}, {NewVDate(d25.Add(1)), false}, {NewVDate(d25.Add(2)), true}, } for _, c := range cases { if c.value.IsOdd() != c.expectOdd { t.Errorf("%s should be odd: %v", c.value, c.expectOdd) } } } func TestNext(t *testing.T) { d := NewVDate(date.New(2016, 2, 7)) is(t, d.Next().Day().String(), "2016-02-08") is(t, d.Next().Week().String(), "2016-02-14") is(t, d.Next().Month().String(), "2016-03-07") is(t, d.Next().Year().String(), "2017-02-07") } func TestPrevious(t *testing.T) { d := NewVDate(date.New(2016, 2, 7)) is(t, d.Previous().Day().String(), "2016-02-06") is(t, d.Previous().Week().String(), "2016-01-31") is(t, d.Previous().Month().String(), "2016-01-07") is(t, d.Previous().Year().String(), "2015-02-07") } func is(t *testing.T, s1, s2 string) { t.Helper() if s1 != s2 { t.Errorf("%s != %s", s1, s2) } } func TestJSONMarshalling(t *testing.T) { cases := []struct { value VDate want string }{ {NewVDate(date.New(-1, time.December, 31)), `"-0001-12-31"`}, {NewVDate(date.New(2012, time.June, 25)), `"2012-06-25"`}, {NewVDate(date.New(12345, time.June, 7)), `"+12345-06-07"`}, } for _, c := range cases { var d VDate bytes, err := json.Marshal(c.value) if err != nil { t.Errorf("JSON(%v) marshal error %v", c, err) } else if string(bytes) != c.want { t.Errorf("JSON(%v) == %v, want %v", c.value, string(bytes), c.want) } else { err = json.Unmarshal(bytes, &d) if err != nil { t.Errorf("JSON(%v) unmarshal error %v", c.value, err) } else if d != c.value { t.Errorf("JSON(%#v) unmarshal got %#v", c.value, d) } } } } func TestTextMarshalling(t *testing.T) { cases := []struct { value VDate want string }{ {NewVDate(date.New(-1, time.December, 31)), "-0001-12-31"}, {NewVDate(date.New(2012, time.June, 25)), "2012-06-25"}, {NewVDate(date.New(12345, time.June, 7)), "+12345-06-07"}, } for _, c := range cases { var d VDate bytes, err := c.value.MarshalText() if err != nil { t.Errorf("Text(%v) marshal error %v", c, err) } else if string(bytes) != c.want { t.Errorf("Text(%v) == %v, want %v", c.value, string(bytes), c.want) } else { err = d.UnmarshalText(bytes) if err != nil { t.Errorf("Text(%v) unmarshal error %v", c.value, err) } else if d != c.value { t.Errorf("Text(%#v) unmarshal got %#v", c.value, d) } } } }