]> Cypherpunks.ru repositories - gotai64n.git/commitdiff
Leapsecs and TAI64 support v2.0.0
authorSergey Matveev <stargrave@stargrave.org>
Wed, 28 Apr 2021 14:01:39 +0000 (17:01 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Wed, 28 Apr 2021 14:10:07 +0000 (17:10 +0300)
README
cmd/leapsecsdb/main.go [new file with mode: 0644]
cmd/tai64nlocal/main.go
db.go [new file with mode: 0644]
go.mod
leapsecs.go [new file with mode: 0644]
tai64n.go
tai64n_test.go [new file with mode: 0644]

diff --git a/README b/README
index 21988c8795434330ea81e2a3efc8008e9503d3a4..60e3fd9276c2e8821027a9caffa8ec4ca28204f8 100644 (file)
--- a/README
+++ b/README
@@ -1,9 +1,3 @@
-Pure Go TAI64N (http://cr.yp.to/libtai/tai64.html) implementation.
-cmd/tai64nlocal contains similar to DJB's one utility.
-Example TAI64N creation:
-
-    import "go.cypherpunks.ru/tai64n"
-
-    tai := new(tai64n.TAI64N)
-    tai64n.FromTime(time.Now(), tai)
-    println(tai.Encode()) // @400000005fd24ce33323c4d1
+Pure Go TAI64/TAI64N (http://cr.yp.to/libtai/tai64.html) implementation.
+cmd/tai64nlocal and cmd/leapsecsdb are similar to DJB's ones.
+Look at cmd/* and docstrings for example usage.
diff --git a/cmd/leapsecsdb/main.go b/cmd/leapsecsdb/main.go
new file mode 100644 (file)
index 0000000..82c5295
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+go.cypherpunks.ru/tai64n -- Pure Go TAI64/TAI64N implementation
+Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package main
+
+import (
+       "bufio"
+       "flag"
+       "fmt"
+       "log"
+       "os"
+       "time"
+
+       "go.cypherpunks.ru/tai64n/v2"
+)
+
+func main() {
+       log.SetFlags(0)
+       flag.Usage = func() {
+               fmt.Fprintf(
+                       flag.CommandLine.Output(),
+                       `Convert YYYY-MM-DD dates to TAI64 timestamps.
+$ leapsecsdb > leapsecs.dat <<EOF
+1972-07-01
+1973-01-01
+EOF
+`,
+               )
+               flag.PrintDefaults()
+       }
+       flag.Parse()
+
+       scanner := bufio.NewScanner(os.Stdin)
+       var err error
+       var t time.Time
+       tai := new(tai64n.TAI64)
+       for {
+               if !scanner.Scan() {
+                       if err = scanner.Err(); err != nil {
+                               log.Fatalln(err)
+                       }
+                       break
+               }
+               t, err = time.Parse("2006-01-02", scanner.Text())
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               tai.FromTime(t)
+               if _, err = os.Stdout.Write(tai[:]); err != nil {
+                       log.Fatalln(err)
+               }
+       }
+}
index 4b58127a6fdfabd0a74da6ee3c64a835b8bbb340..b2a0c155d8f2220561197c19b445d80cec5322e8 100644 (file)
@@ -1,5 +1,5 @@
 /*
-go.cypherpunks.ru/tai64n -- Pure Go TAI64N implementation
+go.cypherpunks.ru/tai64n -- Pure Go TAI64/TAI64N implementation
 Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
@@ -19,16 +19,38 @@ package main
 
 import (
        "bufio"
+       "flag"
+       "fmt"
+       "io/ioutil"
        "log"
        "os"
        "strings"
        "time"
 
-       "go.cypherpunks.ru/tai64n"
+       "go.cypherpunks.ru/tai64n/v2"
 )
 
 func main() {
        log.SetFlags(0)
+       flag.Usage = func() {
+               fmt.Fprintf(
+                       flag.CommandLine.Output(),
+                       "Replace \"@HEX(TAI64)\"-prefixed line with human readable UTC.\n",
+               )
+               flag.PrintDefaults()
+       }
+       leapsecs := flag.Bool("leapsecs", false, "Take leap seconds into account: honest TAI->UTC")
+       db := flag.String("leapsecsdb", "", "Use that leapsecs.dat leap seconds database")
+       flag.Parse()
+
+       if *db != "" {
+               buf, err := ioutil.ReadFile(*db)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               tai64n.LeapsecsDBLoad(buf)
+       }
+
        scanner := bufio.NewScanner(os.Stdin)
        var err error
        var s string
@@ -55,6 +77,9 @@ func main() {
                if err != nil {
                        log.Fatalln(err)
                }
+               if *leapsecs {
+                       t = tai64n.LeapsecsSub(t)
+               }
                os.Stdout.WriteString(t.Format(tai64n.LocalFmt) + s[sep:] + "\n")
        }
 }
diff --git a/db.go b/db.go
new file mode 100644 (file)
index 0000000..a9a9e35
--- /dev/null
+++ b/db.go
@@ -0,0 +1,52 @@
+/*
+go.cypherpunks.ru/tai64n -- Pure Go TAI64/TAI64N implementation
+Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package tai64n
+
+import "time"
+
+func init() {
+       LeapsecsDB = []int64{
+               time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(2015, 7, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(2012, 7, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(2009, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(2006, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1997, 7, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1996, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1994, 7, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1993, 7, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1992, 7, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1991, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1988, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1985, 7, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1983, 7, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1982, 7, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1981, 7, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1979, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1978, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1977, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1976, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1975, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1974, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1973, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
+               time.Date(1972, 7, 1, 0, 0, 0, 0, time.UTC).Unix(),
+       }
+}
diff --git a/go.mod b/go.mod
index 60f8dc80e7decfa1e211ead192a87b8671b2acfa..e0e9206ada59c9e5681ae8c1b6cfa5f7c02f94d2 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
-module go.cypherpunks.ru/tai64n
+module go.cypherpunks.ru/tai64n/v2
 
 go 1.12
diff --git a/leapsecs.go b/leapsecs.go
new file mode 100644 (file)
index 0000000..fe80b8a
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+go.cypherpunks.ru/tai64n -- Pure Go TAI64/TAI64N implementation
+Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package tai64n
+
+import (
+       "sort"
+       "time"
+)
+
+const Leapsecs1972 = 10
+
+// Database of Unix timestamps of the time when leap second occurred.
+// Library contains and initializes it with leap seconds up to 2016-12-31.
+var LeapsecsDB []int64
+
+// TAI<->UTC difference for the given Unix timestamp.
+func LeapsecsDiff(t int64) int {
+       for i, leap := range LeapsecsDB {
+               if t > leap {
+                       return len(LeapsecsDB) - i
+               }
+       }
+       return 0
+}
+
+// Add currently known (LeapsecsDB) leap seconds, not including initial
+// 1972-01-01 10-seconds offset.
+func LeapsecsAdd(t time.Time) time.Time {
+       return t.Add(time.Second * time.Duration(LeapsecsDiff(t.Unix())))
+}
+
+// Opposite of LeapsecsAdd().
+func LeapsecsSub(t time.Time) time.Time {
+       return t.Add(-time.Second * time.Duration(LeapsecsDiff(t.Unix())))
+}
+
+type Int64s []int64
+
+func (a Int64s) Len() int           { return len(a) }
+func (a Int64s) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a Int64s) Less(i, j int) bool { return a[i] > a[j] }
+
+// Load "leapsecs.dat"-like database: concatenated TAI64 leap seconds.
+// Function panics if encoding is invalid.
+func LeapsecsDBLoad(buf []byte) {
+       db := make([]int64, 0, len(buf)/TAI64Size)
+       for i := 0; i < len(buf); i += TAI64Size {
+               db = append(db, (ToTime(buf[i:i+TAI64Size]).Unix()/86400)*86400)
+       }
+       sort.Sort(Int64s(db))
+       LeapsecsDB = db
+}
index 473c7e8083e66db6c9118f2956dce816c29ba798..8af8ab3a349899cddb698e132dfbec7d91f8f4b2 100644 (file)
--- a/tai64n.go
+++ b/tai64n.go
@@ -1,5 +1,5 @@
 /*
-go.cypherpunks.ru/tai64n -- Pure Go TAI64N implementation
+go.cypherpunks.ru/tai64n -- Pure Go TAI64/TAI64N implementation
 Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
@@ -15,6 +15,18 @@ You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
+// TAI64/TAI64N (http://cr.yp.to/libtai/tai64.html) dealing library.
+// You can convert time to TAI64/TAI64N and vice versa with it.
+//
+//     tai := new(tai64n.TAI64N)
+//     tai.FromTime(time.Now())
+//     printable := tai64n.Encode(tai[:])
+//     decoded, err := tai64n.Decode(printable)
+//     tai64n.ToTime(tai[:]) == decoded
+//
+// By default TAI64 timestamps contain initial 1972-01-01 10-seconds
+// TAI<->UTC difference. If you need honest TAI representation, then you
+// should also use Leapsecs* functions.
 package tai64n
 
 import (
@@ -26,38 +38,57 @@ import (
 )
 
 const (
-       Size     = 12
-       Base     = 0x400000000000000a
-       LocalFmt = "2006-01-02 15:04:05.000000000"
+       TAI64Size  = 8
+       TAI64NSize = TAI64Size + 4
+       LocalFmt   = "2006-01-02 15:04:05.000000000"
+       Base       = 0x4000000000000000 + Leapsecs1972
 )
 
-type TAI64N [Size]byte
+type TAI64 [TAI64Size]byte
+type TAI64N [TAI64NSize]byte
 
-func FromTime(src time.Time, dst *TAI64N) {
+func (dst *TAI64) FromTime(src time.Time) {
+       binary.BigEndian.PutUint64(dst[:], uint64(Base)+uint64(src.Unix()))
+}
+
+func (dst *TAI64N) FromTime(src time.Time) {
        binary.BigEndian.PutUint64(dst[:], uint64(Base)+uint64(src.Unix()))
        binary.BigEndian.PutUint32(dst[8:], uint32(src.Nanosecond()))
 }
 
 func ToTime(tai []byte) time.Time {
-       if len(tai) != Size {
-               panic("invalid size")
+       var secs, nano int64
+       switch len(tai) {
+       case TAI64NSize:
+               nano = int64(binary.BigEndian.Uint32(tai[8:]))
+               fallthrough
+       case TAI64Size:
+               secs = int64(binary.BigEndian.Uint64(tai[:8])) - Base
+       default:
+               panic("invalid tai size")
+       }
+       if secs < 0 {
+               panic("dates < 1970-01-01 are not supported")
        }
-       secs := int64(binary.BigEndian.Uint64(tai[:8]))
-       nano := int64(binary.BigEndian.Uint32(tai[8:]))
-       return time.Unix(secs-Base, nano)
+       return time.Unix(secs, nano)
 }
 
-func (tai TAI64N) Encode() string {
-       return "@" + hex.EncodeToString(tai[:])
+// Convert TAI64/TAI64N to "@HEX(TAI64)" format.
+func Encode(tai []byte) string {
+       raw := make([]byte, 1+hex.EncodedLen(len(tai)))
+       raw[0] = byte('@')
+       hex.Encode(raw[1:], tai)
+       return string(raw)
 }
 
+// Convert TAI64/TAI64N "@HEX(TAI64)" format to Time.
 func Decode(s string) (time.Time, error) {
-       tai, err := hex.DecodeString(strings.TrimPrefix(s, "@"))
-       if len(tai) != Size {
-               return time.Time{}, errors.New("invalid ts length")
+       raw, err := hex.DecodeString(strings.TrimPrefix(s, "@"))
+       if !(len(raw) == TAI64Size || len(raw) == TAI64NSize) {
+               err = errors.New("invalid length")
        }
-       if err != nil {
-               return time.Time{}, err
+       if err == nil {
+               return ToTime(raw), nil
        }
-       return ToTime(tai), nil
+       return time.Time{}, err
 }
diff --git a/tai64n_test.go b/tai64n_test.go
new file mode 100644 (file)
index 0000000..892f77f
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+go.cypherpunks.ru/tai64n -- Pure Go TAI64/TAI64N implementation
+Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package tai64n
+
+import (
+       "testing"
+       "time"
+)
+
+func TestVector(t *testing.T) {
+       tm, err := Decode("400000002a2b2c2d")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       ref := time.Date(1992, 6, 2, 8, 7, 9, 0, time.UTC).Add(-Leapsecs1972 * time.Second)
+       if !tm.Equal(ref) {
+               t.Fatal("TAI64 != reference")
+       }
+
+       tm = LeapsecsSub(tm)
+       ref = time.Date(1992, 6, 2, 8, 6, 43, 0, time.UTC)
+       if !tm.Equal(ref) {
+               t.Fatal("UTC != reference")
+       }
+}
+
+func BenchmarkTAI64(b *testing.B) {
+       now := time.Now()
+       now = time.Unix(now.Unix(), 0)
+       tai := new(TAI64)
+       b.ResetTimer()
+       for i := 0; i < b.N; i++ {
+               tai.FromTime(now)
+               if !ToTime(tai[:]).Equal(now) {
+                       b.FailNow()
+               }
+       }
+}
+
+func BenchmarkTAI64N(b *testing.B) {
+       now := time.Now()
+       now = time.Unix(now.Unix(), now.UnixNano())
+       tai := new(TAI64N)
+       b.ResetTimer()
+       for i := 0; i < b.N; i++ {
+               tai.FromTime(now)
+               if !ToTime(tai[:]).Equal(now) {
+                       b.FailNow()
+               }
+       }
+}