From 9978840e570dfe3a23e2db36d64237febceaebcf Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Wed, 28 Apr 2021 17:01:39 +0300 Subject: [PATCH] Leapsecs and TAI64 support --- README | 12 ++----- cmd/leapsecsdb/main.go | 67 +++++++++++++++++++++++++++++++++++++++ cmd/tai64nlocal/main.go | 29 +++++++++++++++-- db.go | 52 +++++++++++++++++++++++++++++++ go.mod | 2 +- leapsecs.go | 67 +++++++++++++++++++++++++++++++++++++++ tai64n.go | 69 +++++++++++++++++++++++++++++------------ tai64n_test.go | 67 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 334 insertions(+), 31 deletions(-) create mode 100644 cmd/leapsecsdb/main.go create mode 100644 db.go create mode 100644 leapsecs.go create mode 100644 tai64n_test.go diff --git a/README b/README index 21988c8..60e3fd9 100644 --- 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 index 0000000..82c5295 --- /dev/null +++ b/cmd/leapsecsdb/main.go @@ -0,0 +1,67 @@ +/* +go.cypherpunks.ru/tai64n -- Pure Go TAI64/TAI64N implementation +Copyright (C) 2020-2021 Sergey Matveev + +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 . +*/ + +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 < 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 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 + +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 . +*/ + +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 60f8dc8..e0e9206 100644 --- 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 index 0000000..fe80b8a --- /dev/null +++ b/leapsecs.go @@ -0,0 +1,67 @@ +/* +go.cypherpunks.ru/tai64n -- Pure Go TAI64/TAI64N implementation +Copyright (C) 2020-2021 Sergey Matveev + +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 . +*/ + +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 +} diff --git a/tai64n.go b/tai64n.go index 473c7e8..8af8ab3 100644 --- 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 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 . */ +// 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 index 0000000..892f77f --- /dev/null +++ b/tai64n_test.go @@ -0,0 +1,67 @@ +/* +go.cypherpunks.ru/tai64n -- Pure Go TAI64/TAI64N implementation +Copyright (C) 2020-2021 Sergey Matveev + +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 . +*/ + +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() + } + } +} -- 2.44.0