]> Cypherpunks repositories - gotai64n.git/commitdiff
Deal with leap seconds TAI↔UTC conversion v4.0.0
authorSergey Matveev <stargrave@stargrave.org>
Thu, 21 Nov 2024 08:26:49 +0000 (11:26 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Thu, 21 Nov 2024 08:26:49 +0000 (11:26 +0300)
README
cmd/leapsecsdb/main.go [deleted file]
cmd/tai64nlocal/main.go
db.go [deleted file]
go.mod
leapsecs.go
printable.go [new file with mode: 0644]
tai.go [new file with mode: 0644]
tai64n.go [deleted file]
tai64n_test.go [deleted file]
tai_test.go [new file with mode: 0644]

diff --git a/README b/README
index 60e3fd9276c2e8821027a9caffa8ec4ca28204f8..f5e1b6d1a75eb0a64e3adc9351545bd854bd4470 100644 (file)
--- a/README
+++ b/README
@@ -1,3 +1,2 @@
 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.
+cmd/tai64nlocal is similar to DJB's one.
diff --git a/cmd/leapsecsdb/main.go b/cmd/leapsecsdb/main.go
deleted file mode 100644 (file)
index 0c94011..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-// go.cypherpunks.su/tai64n -- Pure Go TAI64/TAI64N implementation
-// Copyright (C) 2020-2024 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.su/tai64n/v3"
-)
-
-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 c9294774e32f5e2cb6bc4699b483dd278c20b6ba..9633724eb7397336d26f7d4c64acf9231502fc4e 100644 (file)
@@ -1,5 +1,5 @@
 // go.cypherpunks.su/tai64n -- Pure Go TAI64/TAI64N implementation
-// Copyright (C) 2020-2024 Sergey Matveev <stargrave@stargrave.org>
+// Copyright (C) 2020-2025 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
@@ -24,7 +24,7 @@ import (
        "strings"
        "time"
 
-       "go.cypherpunks.su/tai64n/v3"
+       "go.cypherpunks.su/tai64n/v4"
 )
 
 func main() {
@@ -45,7 +45,7 @@ func main() {
                if err != nil {
                        log.Fatalln(err)
                }
-               tai64n.LeapsecsDBLoad(buf)
+               tai64n.Leapsecs.Load(buf)
        }
 
        scanner := bufio.NewScanner(os.Stdin)
@@ -75,7 +75,7 @@ func main() {
                        log.Fatalln(err)
                }
                if *leapsecs {
-                       t = tai64n.LeapsecsSub(t)
+                       t, _ = tai64n.Leapsecs.Sub(t)
                }
                os.Stdout.WriteString(t.Format(tai64n.LocalFmt) + s[sep:] + "\n")
        }
diff --git a/db.go b/db.go
deleted file mode 100644 (file)
index f604532..0000000
--- a/db.go
+++ /dev/null
@@ -1,50 +0,0 @@
-// go.cypherpunks.su/tai64n -- Pure Go TAI64/TAI64N implementation
-// Copyright (C) 2020-2024 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 954cb2ed11582b9a8a7a044513466909d631383d..a816341b963a1c423c0f04b0acc10b7d5e791b3a 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
-module go.cypherpunks.su/tai64n/v3
+module go.cypherpunks.su/tai64n/v4
 
 go 1.20
index f07ba10caa40553887e286a7452fa94601782d15..4bad6a2e415bcffd33823482fd2d2d8f6bbad569 100644 (file)
@@ -1,5 +1,5 @@
 // go.cypherpunks.su/tai64n -- Pure Go TAI64/TAI64N implementation
-// Copyright (C) 2020-2024 Sergey Matveev <stargrave@stargrave.org>
+// Copyright (C) 2020-2025 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
 package tai64n
 
 import (
-       "sort"
        "time"
 )
 
 const Leapsecs1972 = 10
 
+type LeapsecsList []int64
+
 // 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
+var Leapsecs LeapsecsList
 
-// 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
+// Add leap seconds to the time (convert TAI to UTC).
+func (leapsecs LeapsecsList) Add(tai time.Time) (utc time.Time) {
+       var i int
+       v := tai.Unix()
+       for ; i < len(leapsecs); i++ {
+               if v < leapsecs[i] {
+                       break
                }
        }
-       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())))
+       diff := Leapsecs1972 + int64(i)
+       if v+diff == leapsecs[i] {
+               diff++
+       }
+       utc = tai.Add(time.Second * time.Duration(diff))
+       return
 }
 
-// Opposite of LeapsecsAdd().
-func LeapsecsSub(t time.Time) time.Time {
-       return t.Add(-time.Second * time.Duration(LeapsecsDiff(t.Unix())))
+// Subtract leap seconds from the time (convert UTC to TAI).
+func (leapsecs LeapsecsList) Sub(utc time.Time) (tai time.Time, isLeap bool) {
+       diff := int64(Leapsecs1972)
+       v := utc.Unix()
+       for _, leapsec := range leapsecs {
+               if v < leapsec {
+                       break
+               }
+               diff++
+               if v == leapsec {
+                       isLeap = true
+                       break
+               }
+       }
+       tai = utc.Add(-time.Second * time.Duration(diff))
+       return
 }
 
-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) {
+// Load "leapsecs.dat"-like database (concatenated TAI64 leap seconds).
+func (leapsecs *LeapsecsList) Load(buf []byte) {
        db := make([]int64, 0, len(buf)/TAI64Size)
+       var tai TAI64
        for i := 0; i < len(buf); i += TAI64Size {
-               db = append(db, (ToTime(buf[i:i+TAI64Size]).Unix()/86400)*86400)
+               tai = [TAI64Size]byte(buf[i : i+TAI64Size])
+               db = append(db, tai.Time().Unix())
+       }
+       *leapsecs = db
+}
+
+func init() {
+       def := []time.Time{
+               time.Date(1972, 6, 30, 0, 0, 0, 0, time.UTC),
+               time.Date(1972, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1973, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1974, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1975, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1976, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1977, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1978, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1979, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1981, 6, 30, 0, 0, 0, 0, time.UTC),
+               time.Date(1982, 6, 30, 0, 0, 0, 0, time.UTC),
+               time.Date(1983, 6, 30, 0, 0, 0, 0, time.UTC),
+               time.Date(1985, 6, 30, 0, 0, 0, 0, time.UTC),
+               time.Date(1987, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1989, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1990, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1992, 6, 30, 0, 0, 0, 0, time.UTC),
+               time.Date(1993, 6, 30, 0, 0, 0, 0, time.UTC),
+               time.Date(1994, 6, 30, 0, 0, 0, 0, time.UTC),
+               time.Date(1995, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(1997, 6, 30, 0, 0, 0, 0, time.UTC),
+               time.Date(1998, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(2005, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(2008, 12, 31, 0, 0, 0, 0, time.UTC),
+               time.Date(2012, 6, 30, 0, 0, 0, 0, time.UTC),
+               time.Date(2015, 6, 30, 0, 0, 0, 0, time.UTC),
+               time.Date(2016, 12, 31, 0, 0, 0, 0, time.UTC),
+       }
+       db := make([]int64, len(def))
+       for i, t := range def {
+               db[i] = t.Add(time.Second*86400).Unix() + Leapsecs1972 + int64(i)
        }
-       sort.Sort(Int64s(db))
-       LeapsecsDB = db
+       Leapsecs = LeapsecsList(db)
 }
diff --git a/printable.go b/printable.go
new file mode 100644 (file)
index 0000000..eaad01d
--- /dev/null
@@ -0,0 +1,56 @@
+// go.cypherpunks.su/tai64n -- Pure Go TAI64/TAI64N implementation
+// Copyright (C) 2020-2025 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 (
+       "encoding/hex"
+       "errors"
+       "strings"
+       "time"
+)
+
+func (tai *TAI64) Encode() string {
+       raw := make([]byte, 1+TAI64Size*2)
+       raw[0] = '@'
+       hex.Encode(raw[1:], tai[:])
+       return string(raw)
+}
+
+func (tai *TAI64N) Encode() string {
+       raw := make([]byte, 1+TAI64NSize*2)
+       raw[0] = '@'
+       hex.Encode(raw[1:], tai[:])
+       return string(raw)
+}
+
+// Convert "@HEX(TAI64(N?))" format to Time.
+func Decode(s string) (t time.Time, err error) {
+       var raw []byte
+       raw, err = hex.DecodeString(strings.TrimPrefix(s, "@"))
+       if err != nil {
+               return
+       }
+       switch len(raw) {
+       case TAI64Size:
+               tai := TAI64(raw)
+               return tai.Time(), nil
+       case TAI64NSize:
+               tai := TAI64N(raw)
+               return tai.Time(), nil
+       }
+       err = errors.New("unsupported length")
+       return
+}
diff --git a/tai.go b/tai.go
new file mode 100644 (file)
index 0000000..54142a7
--- /dev/null
+++ b/tai.go
@@ -0,0 +1,63 @@
+// go.cypherpunks.su/tai64n -- Pure Go TAI64/TAI64N implementation
+// Copyright (C) 2020-2025 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/>.
+
+// TAI64/TAI64N (http://cr.yp.to/libtai/tai64.html) dealing library.
+// You can convert time to TAI64/TAI64N and vice versa with it.
+//
+//     var tai tai64n.TAI64N
+//     tai.FromTime(time.Now())
+//     printable := tai.Encode()
+//     decoded, err := tai64n.Decode(printable)
+//     tai.Time() == decoded
+package tai64n
+
+import (
+       "encoding/binary"
+       "time"
+)
+
+const (
+       TAI64Size   = 8
+       TAI64NSize  = TAI64Size + 4
+       TAI64NASize = TAI64NSize + 4
+       LocalFmt    = "2006-01-02 15:04:05.000000000"
+       Base        = 0x4000000000000000
+)
+
+type (
+       TAI64   [TAI64Size]byte
+       TAI64N  [TAI64NSize]byte
+       TAI64NA [TAI64NASize]byte
+)
+
+func (tai *TAI64) FromTime(src time.Time) {
+       binary.BigEndian.PutUint64(tai[:], uint64(Base)+uint64(src.Unix()))
+}
+
+func (tai *TAI64N) FromTime(src time.Time) {
+       binary.BigEndian.PutUint64(tai[:], uint64(Base)+uint64(src.Unix()))
+       binary.BigEndian.PutUint32(tai[TAI64Size:], uint32(src.Nanosecond()))
+}
+
+func (tai *TAI64) Time() time.Time {
+       return time.Unix(int64(binary.BigEndian.Uint64(tai[:TAI64Size]))-Base, 0)
+}
+
+func (tai *TAI64N) Time() time.Time {
+       return time.Unix(
+               int64(binary.BigEndian.Uint64(tai[:TAI64Size]))-Base,
+               int64(binary.BigEndian.Uint32(tai[TAI64Size:])),
+       )
+}
diff --git a/tai64n.go b/tai64n.go
deleted file mode 100644 (file)
index 4f9c3e8..0000000
--- a/tai64n.go
+++ /dev/null
@@ -1,98 +0,0 @@
-// go.cypherpunks.su/tai64n -- Pure Go TAI64/TAI64N implementation
-// Copyright (C) 2020-2024 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/>.
-
-// 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 (
-       "encoding/binary"
-       "encoding/hex"
-       "errors"
-       "strings"
-       "time"
-)
-
-const (
-       TAI64Size   = 8
-       TAI64NSize  = TAI64Size + 4
-       TAI64NASize = TAI64Size + 4 + 4
-       LocalFmt    = "2006-01-02 15:04:05.000000000"
-       Base        = 0x4000000000000000 + Leapsecs1972
-)
-
-type (
-       TAI64   [TAI64Size]byte
-       TAI64N  [TAI64NSize]byte
-       TAI64NA [TAI64NASize]byte
-)
-
-func (tai *TAI64) FromTime(src time.Time) {
-       binary.BigEndian.PutUint64(tai[:], uint64(Base)+uint64(src.Unix()))
-}
-
-func (tai *TAI64N) FromTime(src time.Time) {
-       binary.BigEndian.PutUint64(tai[:], uint64(Base)+uint64(src.Unix()))
-       binary.BigEndian.PutUint32(tai[8:], uint32(src.Nanosecond()))
-}
-
-func ToTime(tai []byte) time.Time {
-       var secs, nano int64
-       switch len(tai) {
-       case TAI64NASize:
-               panic("TAI64NA can not be converted to time.Time")
-       case TAI64NSize:
-               nano = int64(binary.BigEndian.Uint32(tai[8:]))
-               fallthrough
-       case TAI64Size:
-               secs = int64(binary.BigEndian.Uint64(tai[:8])) - Base
-       default:
-               panic("invalid TAI64* size")
-       }
-       return time.Unix(secs, nano)
-}
-
-// Convert TAI64* to "@HEX(TAI64)" format.
-func Encode(tai []byte) string {
-       raw := make([]byte, 1+hex.EncodedLen(len(tai)))
-       raw[0] = '@'
-       hex.Encode(raw[1:], tai)
-       return string(raw)
-}
-
-// Convert TAI64* "@HEX(TAI64)" format to Time.
-func Decode(s string) (t time.Time, err error) {
-       raw, err := hex.DecodeString(strings.TrimPrefix(s, "@"))
-       switch len(raw) {
-       case TAI64NASize:
-       case TAI64NSize:
-       case TAI64Size:
-       default:
-               err = errors.New("invalid length")
-               return
-       }
-       t = ToTime(raw)
-       return
-}
diff --git a/tai64n_test.go b/tai64n_test.go
deleted file mode 100644 (file)
index 8a9d8c9..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-// go.cypherpunks.su/tai64n -- Pure Go TAI64/TAI64N implementation
-// Copyright (C) 2020-2024 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()
-               }
-       }
-}
diff --git a/tai_test.go b/tai_test.go
new file mode 100644 (file)
index 0000000..bce160a
--- /dev/null
@@ -0,0 +1,121 @@
+// go.cypherpunks.su/tai64n -- Pure Go TAI64/TAI64N implementation
+// Copyright (C) 2020-2025 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 TestVectors(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)
+       if !tm.Equal(ref) {
+               t.Fatal("TAI64 != reference")
+       }
+       tm, isLeap := Leapsecs.Sub(tm)
+       ref = time.Date(1992, 6, 2, 8, 6, 43, 0, time.UTC)
+       if !tm.Equal(ref) {
+               t.Fatal("UTC != reference")
+       }
+       if isLeap {
+               t.Fatal("unexpectedly leap")
+       }
+
+       tm, err = Decode("4000000034353637")
+       if err != nil {
+               t.Fatal(err)
+       }
+       ref = time.Date(1997, 10, 3, 18, 15, 19, 0, time.UTC)
+       if !tm.Equal(ref) {
+               t.Fatal("TAI64 != reference")
+       }
+       tm, isLeap = Leapsecs.Sub(tm)
+       ref = time.Date(1997, 10, 3, 18, 14, 48, 0, time.UTC)
+       if !tm.Equal(ref) {
+               t.Fatal("UTC != reference")
+       }
+       if isLeap {
+               t.Fatal("unexpectedly leap")
+       }
+
+       tm, err = Decode("40000000586846a3")
+       if err != nil {
+               t.Fatal(err)
+       }
+       tm, isLeap = Leapsecs.Sub(tm)
+       ref = time.Date(2016, 12, 31, 23, 59, 59, 0, time.UTC)
+       if !tm.Equal(ref) {
+               t.Fatal("UTC != reference")
+       }
+       if isLeap {
+               t.Fatal("unexpectedly leap")
+       }
+
+       tm, err = Decode("40000000586846a4")
+       if err != nil {
+               t.Fatal(err)
+       }
+       tm, isLeap = Leapsecs.Sub(tm)
+       ref = time.Date(2016, 12, 31, 23, 59, 59, 0, time.UTC)
+       if !tm.Equal(ref) {
+               t.Fatal("UTC != reference")
+       }
+       if !isLeap {
+               t.Fatal("unexpectedly non-leap")
+       }
+
+       tm, err = Decode("40000000586846a5")
+       if err != nil {
+               t.Fatal(err)
+       }
+       tm, isLeap = Leapsecs.Sub(tm)
+       ref = time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC)
+       if !tm.Equal(ref) {
+               t.Fatal("UTC != reference")
+       }
+       if isLeap {
+               t.Fatal("unexpectedly leap")
+       }
+}
+
+func BenchmarkTAI64(b *testing.B) {
+       now := time.Now()
+       now = now.Truncate(time.Second)
+       var tai TAI64
+       b.ResetTimer()
+       for i := 0; i < b.N; i++ {
+               tai.FromTime(now)
+               if !tai.Time().Equal(now) {
+                       b.FailNow()
+               }
+       }
+}
+
+func BenchmarkTAI64N(b *testing.B) {
+       now := time.Now()
+       var tai TAI64N
+       b.ResetTimer()
+       for i := 0; i < b.N; i++ {
+               tai.FromTime(now)
+               if !tai.Time().Equal(now) {
+                       b.FailNow()
+               }
+       }
+}