diff --git a/duration/big/big_suite_test.go b/duration/big/big_suite_test.go new file mode 100644 index 0000000..b3b0cc4 --- /dev/null +++ b/duration/big/big_suite_test.go @@ -0,0 +1,53 @@ +/* + * MIT License + * + * Copyright (c) 2023 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + */ + +package big_test + +import ( + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +/* + Using https://onsi.github.io/ginkgo/ + Running with $> ginkgo -cover . +*/ + +// TestGolibEncodingHexHelper tests the Golib Hex Encoding Helper function. +func TestGolibDurationBigHelper(t *testing.T) { + time.Sleep(500 * time.Millisecond) // Adding delay for better testing synchronization + RegisterFailHandler(Fail) // Registering fail handler for better test failure reporting + RunSpecs(t, "Duration Big Helper Suite") // Running the test suite for Encoding Hex Helper +} + +var _ = BeforeSuite(func() { +}) + +var _ = AfterSuite(func() { +}) diff --git a/duration/big/big_test.go b/duration/big/big_test.go new file mode 100644 index 0000000..6c64b8e --- /dev/null +++ b/duration/big/big_test.go @@ -0,0 +1,130 @@ +/* + * MIT License + * + * Copyright (c) 2023 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + */ + +package big_test + +import ( + "encoding/json" + + durbig "github.com/nabbar/golib/duration/big" + "github.com/pelletier/go-toml" + "gopkg.in/yaml.v3" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +type StructExample struct { + Value durbig.Duration `json:"value" yaml:"value" toml:"value"` +} + +var valueExample = StructExample{Value: (5 * durbig.Day) + (23 * durbig.Hour) + (15 * durbig.Minute) + (13 * durbig.Second)} + +func jsonDuration() []byte { + return []byte(`{"value":"5d23h15m13s"}`) +} + +func yamlDuration() []byte { + return []byte(`value: 5d23h15m13s +`) +} + +func tomlDuration() []byte { + return []byte(`value = "5d23h15m13s" +`) +} + +var _ = Describe("duration/big", func() { + Context("decoding value from json, yaml, toml", func() { + var ( + err error + obj = StructExample{} + ) + + It("success when json decoding", func() { + err = json.Unmarshal(jsonDuration(), &obj) + Expect(err).ToNot(HaveOccurred()) + Expect(obj.Value).To(Equal(valueExample.Value)) + }) + + It("success when yaml decoding", func() { + err = yaml.Unmarshal(yamlDuration(), &obj) + Expect(err).ToNot(HaveOccurred()) + Expect(obj.Value).To(Equal(valueExample.Value)) + }) + + It("success when toml decoding", func() { + err = toml.Unmarshal(tomlDuration(), &obj) + Expect(err).ToNot(HaveOccurred()) + Expect(obj.Value).To(Equal(valueExample.Value)) + }) + }) + Context("encoding value from json, yaml, toml", func() { + var ( + err error + res []byte + str string + exp string + ) + + It("success when json encoding", func() { + res, err = json.Marshal(&valueExample) + str = string(res) + exp = string(jsonDuration()) + + Expect(err).ToNot(HaveOccurred()) + Expect(str).To(Equal(exp)) + }) + + It("success when yaml encoding", func() { + res, err = yaml.Marshal(&valueExample) + str = string(res) + exp = string(yamlDuration()) + + Expect(err).ToNot(HaveOccurred()) + Expect(str).To(Equal(exp)) + }) + + It("success when toml encoding", func() { + res, err = toml.Marshal(&valueExample) + str = string(res) + exp = string(tomlDuration()) + + Expect(err).ToNot(HaveOccurred()) + Expect(str).To(Equal(exp)) + }) + + It("success when json encoding of maxDuration Value", func() { + var dur durbig.Duration = 1<<63 - 1 + res, err = json.Marshal(&dur) + str = string(res) + exp = `"106751991167300d15h30m7s"` + + Expect(err).ToNot(HaveOccurred()) + Expect(str).To(Equal(exp)) + }) + }) +}) diff --git a/duration/big/encode.go b/duration/big/encode.go new file mode 100644 index 0000000..68c1a09 --- /dev/null +++ b/duration/big/encode.go @@ -0,0 +1,86 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package big + +import ( + "encoding/json" + "fmt" + + "github.com/fxamacker/cbor/v2" + "gopkg.in/yaml.v3" +) + +func (d Duration) MarshalJSON() ([]byte, error) { + var s = d.String() + return json.Marshal(s) +} + +func (d *Duration) UnmarshalJSON(bytes []byte) error { + return d.unmarshall(bytes) +} + +func (d Duration) MarshalYAML() (interface{}, error) { + var s = d.String() + return s, nil +} + +func (d *Duration) UnmarshalYAML(value *yaml.Node) error { + return d.unmarshall([]byte(value.Value)) +} + +func (d Duration) MarshalTOML() ([]byte, error) { + var s = d.String() + return []byte("\"" + s + "\""), nil +} + +func (d *Duration) UnmarshalTOML(i interface{}) error { + if b, k := i.([]byte); k { + return d.unmarshall(b) + } + + if b, k := i.(string); k { + return d.parseString(b) + } + + return fmt.Errorf("size: value not in valid format") +} + +func (d Duration) MarshalText() ([]byte, error) { + return []byte(d.String()), nil +} + +func (d *Duration) UnmarshalText(bytes []byte) error { + return d.unmarshall(bytes) +} + +func (d Duration) MarshalCBOR() ([]byte, error) { + var s = d.String() + return cbor.Marshal(s) +} + +func (d *Duration) UnmarshalCBOR(bytes []byte) error { + return d.unmarshall(bytes) +} diff --git a/duration/big/format.go b/duration/big/format.go new file mode 100644 index 0000000..01b6b14 --- /dev/null +++ b/duration/big/format.go @@ -0,0 +1,104 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package big + +import ( + "fmt" + "math" + "time" +) + +func (d Duration) Time() (time.Duration, error) { + mxt := float64(math.MaxInt64) / float64(time.Second) + if d.Float64() > mxt { + return 0, fmt.Errorf("overflow max time.Duration value") + } + + return time.Duration(d) * time.Second, nil +} + +// String returns a string representing the duration in the form "135d72h3m5s". +// Leading zero units are omitted. The zero duration formats as 0s. +func (d Duration) String() string { + var s string + + if d < 0 { + s = "-" + } else if d == 0 { + return "0s" + } + + // Days + r, p := stringUnit(int64(d.Abs()), Day.Int64(), "d") + s += p + + // Hours + r, p = stringUnit(r, Hour.Int64(), "h") + s += p + + // Minutes + r, p = stringUnit(r, Minute.Int64(), "m") + s += p + + // Seconds + if r > 0 { + s += fmt.Sprintf("%ds", r) + } + + return s +} + +func stringUnit(val, div int64, unit string) (rest int64, str string) { + if val == val%div { + // same value so no unit in value, so skip + return val, "" + } + + n := val % div + v := (val - n) / div + + if v > 0 { + return n, fmt.Sprintf("%d%s", v, unit) + } else { + return val, "" + } +} + +func (d Duration) Int64() int64 { + return int64(d) +} + +func (d Duration) Uint64() uint64 { + if d < 0 { + return uint64(0) + } + + return uint64(d) +} + +func (d Duration) Float64() float64 { + return float64(d) +} diff --git a/duration/big/interface.go b/duration/big/interface.go new file mode 100644 index 0000000..8dc16d5 --- /dev/null +++ b/duration/big/interface.go @@ -0,0 +1,89 @@ +/*********************************************************************************************************************** + * + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + **********************************************************************************************************************/ + +package big + +import ( + "errors" + "math" + "time" +) + +const mx float64 = math.MaxInt64 + +const ( + Second Duration = 1 + Minute = 60 * Second + Hour = 60 * Minute + Day = 24 * Hour +) + +var ( + ErrOverFlow = errors.New("value overflow max int64") +) + +// Max Value of Big Duration : 106,751,991,167,300 d 15 h 30 m 7 s + +type Duration time.Duration + +func Parse(s string) (Duration, error) { + return parseString(s) +} + +func ParseByte(p []byte) (Duration, error) { + return parseString(string(p)) +} + +func Seconds(i int64) Duration { + return Duration(i) +} + +func Minutes(i int64) Duration { + return Duration(i) * Minute +} + +func Hours(i int64) Duration { + return Duration(i) * Hour +} + +func Days(i int64) Duration { + return Duration(i) * Day +} + +func ParseDuration(d time.Duration) Duration { + return ParseFloat64(math.Floor(d.Seconds())) +} + +func ParseFloat64(f float64) Duration { + const mx float64 = math.MaxInt64 + + if f > mx { + return Duration(math.MaxInt64) + } else { + return Duration(math.Round(f)) + } +} diff --git a/duration/big/model.go b/duration/big/model.go new file mode 100644 index 0000000..9ac0c82 --- /dev/null +++ b/duration/big/model.go @@ -0,0 +1,62 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package big + +import ( + "reflect" + + libmap "github.com/mitchellh/mapstructure" +) + +const ( + minDuration Duration = -1 << 63 + maxDuration Duration = 1<<63 - 1 +) + +func ViperDecoderHook() libmap.DecodeHookFuncType { + return func(from reflect.Type, to reflect.Type, data interface{}) (interface{}, error) { + var ( + z = Duration(0) + t string + k bool + ) + + // Check if the data type matches the expected one + if from.Kind() != reflect.String { + return data, nil + } else if t, k = data.(string); !k { + return data, nil + } + + // Check if the target type matches the expected one + if to != reflect.TypeOf(z) { + return data, nil + } + + // Format/decode/parse the data and return the new value + return parseString(t) + } +} diff --git a/duration/big/operation.go b/duration/big/operation.go new file mode 100644 index 0000000..d3f587d --- /dev/null +++ b/duration/big/operation.go @@ -0,0 +1,108 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package big + +import ( + libpid "github.com/nabbar/golib/pidcontroller" +) + +var ( + DefaultRateProportional float64 = 0.1 + DefaultRateIntegral float64 = 0.01 + DefaultRateDerivative float64 = 0.05 +) + +// Abs returns the absolute value of d. +// As a special case, Duration([math.MinInt64]) is converted to Duration([math.MaxInt64]), +// reducing its magnitude by 1 nanosecond. +func (d Duration) Abs() Duration { + switch { + case d >= 0: + return d + case d == minDuration: + return maxDuration + default: + return -d + } +} + +func (d Duration) RangeTo(dur Duration, rateP, rateI, rateD float64) []Duration { + var ( + p = libpid.New(rateP, rateI, rateD) + r = make([]Duration, 0) + ) + + for _, v := range p.Range(d.Float64(), dur.Float64()) { + r = append(r, ParseFloat64(v)) + } + + if len(r) < 3 { + r = append(make([]Duration, 0), d, dur) + } + + if r[0] > d { + r = append(append(make([]Duration, 0), d), r...) + } + + if r[len(r)-1] < dur { + r = append(r, dur) + } + + return r +} + +func (d Duration) RangeDefTo(dur Duration) []Duration { + return d.RangeTo(dur, DefaultRateProportional, DefaultRateIntegral, DefaultRateDerivative) +} + +func (d Duration) RangeFrom(dur Duration, rateP, rateI, rateD float64) []Duration { + var ( + p = libpid.New(rateP, rateI, rateD) + r = make([]Duration, 0) + ) + + for _, v := range p.Range(dur.Float64(), d.Float64()) { + r = append(r, ParseFloat64(v)) + } + + if len(r) < 3 { + r = append(make([]Duration, 0), d, dur) + } + + if r[0] > dur { + r = append(append(make([]Duration, 0), dur), r...) + } + + if r[len(r)-1] < d { + r = append(r, d) + } + + return r +} + +func (d Duration) RangeDefFrom(dur Duration) []Duration { + return d.RangeFrom(dur, DefaultRateProportional, DefaultRateIntegral, DefaultRateDerivative) +} diff --git a/duration/big/parse.go b/duration/big/parse.go new file mode 100644 index 0000000..99d5b83 --- /dev/null +++ b/duration/big/parse.go @@ -0,0 +1,244 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package big + +import ( + "errors" + "fmt" + "strings" +) + +var errLeadingInt = errors.New("time: bad [0-9]*") // never printed +var unitMap = map[string]uint64{ + "s": uint64(Second), + "m": uint64(Minute), + "h": uint64(Hour), + "d": uint64(Day), +} + +func parseString(s string) (Duration, error) { + s = strings.Replace(s, "\"", "", -1) + s = strings.Replace(s, "'", "", -1) + s = strings.Replace(s, " ", "", -1) + + // err: 99d55h44m33s123ms + return parseDuration(s) +} + +func (d *Duration) parseString(s string) error { + if v, e := parseString(s); e != nil { + return e + } else { + *d = v + return nil + } +} + +func (d *Duration) unmarshall(val []byte) error { + if tmp, err := ParseByte(val); err != nil { + return err + } else { + *d = tmp + return nil + } +} + +// parseDuration parses a duration string. +// code from time.ParseDuration +// A duration string is a possibly signed sequence of +// decimal numbers, each with optional fraction and a unit suffix, +// such as "300ms", "-1.5h" or "2h45m". +// Valid time units are "s", "m", "h", "d". +func parseDuration(s string) (Duration, error) { + // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ + orig := s + var d uint64 + neg := false + + // Consume [-+]? + if s != "" { + c := s[0] + + if c == '-' || c == '+' { + neg = c == '-' + s = s[1:] + } + } + + // Special case: if all that is left is "0", this is zero. + if s == "0" { + return 0, nil + } + + if s == "" { + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + + for s != "" { + var ( + v, f uint64 // integers before, after decimal point + scale float64 = 1 // value = v + f/scale + ) + + var err error + + // The next character must be [0-9.] + if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + + // Consume [0-9]* + pl := len(s) + v, s, err = leadingInt(s) + + if err != nil { + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + + pre := pl != len(s) // whether we consumed anything before a period + + // Consume (\.[0-9]*)? + post := false + + if s != "" && s[0] == '.' { + s = s[1:] + pl := len(s) + f, scale, s = leadingFraction(s) + post = pl != len(s) + } + + if !pre && !post { + // no digits (e.g. ".s" or "-.s") + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + + // Consume unit. + i := 0 + + for ; i < len(s); i++ { + c := s[i] + if c == '.' || '0' <= c && c <= '9' { + break + } + } + + if i == 0 { + return 0, fmt.Errorf("time: missing unit in duration '%s'" + orig) + } + + u := s[:i] + s = s[i:] + unit, ok := unitMap[u] + + if !ok { + return 0, fmt.Errorf("time: unknown unit '%s' in duration '%s'", u, orig) + } + + if v > 1<<63/unit { + // overflow + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + + v *= unit + + if f > 0 { + // float64 is needed to be nanosecond accurate for fractions of hours. + // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) + v += uint64(float64(f) * (float64(unit) / scale)) + + if v > 1<<63 { + // overflow + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + } + d += v + + if d > 1<<63 { + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + } + + if neg { + return -Duration(d), nil + } + + if d > 1<<63-1 { + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + return Duration(d), nil +} + +// leadingInt consumes the leading [0-9]* from s. +func leadingInt[bytes []byte | string](s bytes) (x uint64, rem bytes, err error) { + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if x > 1<<63/10 { + // overflow + return 0, rem, errLeadingInt + } + x = x*10 + uint64(c) - '0' + if x > 1<<63 { + // overflow + return 0, rem, errLeadingInt + } + } + return x, s[i:], nil +} + +// leadingFraction consumes the leading [0-9]* from s. +// It is used only for fractions, so does not return an error on overflow, +// it just stops accumulating precision. +func leadingFraction(s string) (x uint64, scale float64, rem string) { + i := 0 + scale = 1 + overflow := false + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if overflow { + continue + } + if x > (1<<63-1)/10 { + // It's possible for overflow to give a positive number, so take care. + overflow = true + continue + } + y := x*10 + uint64(c) - '0' + if y > 1<<63 { + overflow = true + continue + } + x = y + scale *= 10 + } + return x, scale, s[i:] +} diff --git a/duration/big/truncate.go b/duration/big/truncate.go new file mode 100644 index 0000000..1c593d2 --- /dev/null +++ b/duration/big/truncate.go @@ -0,0 +1,100 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package big + +// Round returns the result of rounding d to the nearest multiple of unit. +// The rounding behavior for halfway values is to round away from zero. +// If the result exceeds the maximum (or minimum) +// value that can be stored in a [Duration], +// Round returns the maximum (or minimum) duration. +// If unit <= 0, Round returns d unchanged. +// code from time.Duration +func (d Duration) Round(unit Duration) Duration { + if unit <= 0 { + return d + } + + r := d % unit + + if d < 0 { + r = -r + + if lessThanHalf(r, unit) { + return d + r + } + + if d1 := d - unit + r; d1 < d { + return d1 + } + + return minDuration // overflow + } + + if lessThanHalf(r, unit) { + return d - r + } + + if d1 := d + unit - r; d1 > d { + return d1 + } + + return maxDuration // overflow +} + +// Truncate returns the result of rounding d toward zero to a multiple of m. +// If unit <= 0, Truncate returns d unchanged. +// code from time.Duration +func (d Duration) Truncate(unit Duration) Duration { + if unit <= 0 { + return d + } + return d - d%unit +} + +// TruncateMinutes returns the result of rounding d toward zero to a multiple of Minute. +// If unit <= 0, TruncateMinutes returns d unchanged. +func (d Duration) TruncateMinutes() Duration { + return d.Truncate(Minute) +} + +// TruncateHours returns the result of rounding d toward zero to a multiple of Hour. +// If unit <= 0, TruncateHours returns d unchanged. +func (d Duration) TruncateHours() Duration { + return d.Truncate(Hour) +} + +// TruncateDays returns the result of rounding d toward zero to a multiple of Day. +// If unit <= 0, TruncateDays returns d unchanged. +func (d Duration) TruncateDays() Duration { + return d.Truncate(Day) +} + +// lessThanHalf reports whether x+x < y but avoids overflow, +// assuming x and y are both positive (Duration is signed). +// code from time.Duration +func lessThanHalf(x, y Duration) bool { + return uint64(x)+uint64(x) < uint64(y) +} diff --git a/duration/big_suite_test.go b/duration/big_suite_test.go new file mode 100644 index 0000000..fbff8b9 --- /dev/null +++ b/duration/big_suite_test.go @@ -0,0 +1,53 @@ +/* + * MIT License + * + * Copyright (c) 2023 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + */ + +package duration_test + +import ( + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +/* + Using https://onsi.github.io/ginkgo/ + Running with $> ginkgo -cover . +*/ + +// TestGolibEncodingHexHelper tests the Golib Hex Encoding Helper function. +func TestGolibDurationHelper(t *testing.T) { + time.Sleep(500 * time.Millisecond) // Adding delay for better testing synchronization + RegisterFailHandler(Fail) // Registering fail handler for better test failure reporting + RunSpecs(t, "Duration Helper Suite") // Running the test suite for Encoding Hex Helper +} + +var _ = BeforeSuite(func() { +}) + +var _ = AfterSuite(func() { +}) diff --git a/duration/big_test.go b/duration/big_test.go new file mode 100644 index 0000000..4f759bd --- /dev/null +++ b/duration/big_test.go @@ -0,0 +1,120 @@ +/* + * MIT License + * + * Copyright (c) 2023 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + */ + +package duration_test + +import ( + "encoding/json" + + libdur "github.com/nabbar/golib/duration" + "github.com/pelletier/go-toml" + "gopkg.in/yaml.v3" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +type StructExample struct { + Value libdur.Duration `json:"value" yaml:"value" toml:"value"` +} + +var valueExample = StructExample{Value: libdur.Days(5) + libdur.Hours(23) + libdur.Minutes(15) + libdur.Seconds(13)} + +func jsonDuration() []byte { + return []byte(`{"value":"5d23h15m13s"}`) +} + +func yamlDuration() []byte { + return []byte(`value: 5d23h15m13s +`) +} + +func tomlDuration() []byte { + return []byte(`value = "5d23h15m13s" +`) +} + +var _ = Describe("duration/big", func() { + Context("decoding value from json, yaml, toml", func() { + var ( + err error + obj = StructExample{} + ) + + It("success when json decoding", func() { + err = json.Unmarshal(jsonDuration(), &obj) + Expect(err).ToNot(HaveOccurred()) + Expect(obj.Value).To(Equal(valueExample.Value)) + }) + + It("success when yaml decoding", func() { + err = yaml.Unmarshal(yamlDuration(), &obj) + Expect(err).ToNot(HaveOccurred()) + Expect(obj.Value).To(Equal(valueExample.Value)) + }) + + It("success when toml decoding", func() { + err = toml.Unmarshal(tomlDuration(), &obj) + Expect(err).ToNot(HaveOccurred()) + Expect(obj.Value).To(Equal(valueExample.Value)) + }) + }) + Context("encoding value from json, yaml, toml", func() { + var ( + err error + res []byte + str string + exp string + ) + + It("success when json encoding", func() { + res, err = json.Marshal(&valueExample) + str = string(res) + exp = string(jsonDuration()) + + Expect(err).ToNot(HaveOccurred()) + Expect(str).To(Equal(exp)) + }) + + It("success when yaml encoding", func() { + res, err = yaml.Marshal(&valueExample) + str = string(res) + exp = string(yamlDuration()) + + Expect(err).ToNot(HaveOccurred()) + Expect(str).To(Equal(exp)) + }) + + It("success when toml encoding", func() { + res, err = toml.Marshal(&valueExample) + str = string(res) + exp = string(tomlDuration()) + + Expect(err).ToNot(HaveOccurred()) + Expect(str).To(Equal(exp)) + }) + }) +}) diff --git a/duration/encode.go b/duration/encode.go index d1020ee..8aefe20 100644 --- a/duration/encode.go +++ b/duration/encode.go @@ -26,18 +26,16 @@ package duration import ( + "encoding/json" "fmt" + "github.com/fxamacker/cbor/v2" "gopkg.in/yaml.v3" ) func (d Duration) MarshalJSON() ([]byte, error) { - t := d.String() - b := make([]byte, 0, len(t)+2) - b = append(b, '"') - b = append(b, []byte(t)...) - b = append(b, '"') - return b, nil + var s = d.String() + return json.Marshal(s) } func (d *Duration) UnmarshalJSON(bytes []byte) error { @@ -45,7 +43,8 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error { } func (d Duration) MarshalYAML() (interface{}, error) { - return d.MarshalJSON() + var s = d.String() + return s, nil } func (d *Duration) UnmarshalYAML(value *yaml.Node) error { @@ -53,7 +52,8 @@ func (d *Duration) UnmarshalYAML(value *yaml.Node) error { } func (d Duration) MarshalTOML() ([]byte, error) { - return d.MarshalJSON() + var s = d.String() + return []byte("\"" + s + "\""), nil } func (d *Duration) UnmarshalTOML(i interface{}) error { @@ -77,7 +77,8 @@ func (d *Duration) UnmarshalText(bytes []byte) error { } func (d Duration) MarshalCBOR() ([]byte, error) { - return []byte(d.String()), nil + var s = d.String() + return cbor.Marshal(s) } func (d *Duration) UnmarshalCBOR(bytes []byte) error { diff --git a/duration/parse.go b/duration/parse.go index 32c2cfa..d79e281 100644 --- a/duration/parse.go +++ b/duration/parse.go @@ -26,21 +26,32 @@ package duration import ( + "errors" + "fmt" "strings" "time" ) +var errLeadingInt = errors.New("time: bad [0-9]*") // never printed +var unitMap = map[string]uint64{ + "ns": uint64(time.Nanosecond), + "us": uint64(time.Microsecond), + "µs": uint64(time.Microsecond), // U+00B5 = micro symbol + "μs": uint64(time.Microsecond), // U+03BC = Greek letter mu + "ms": uint64(time.Millisecond), + "s": uint64(time.Second), + "m": uint64(time.Minute), + "h": uint64(time.Hour), + "d": uint64(24 * time.Hour), +} + func parseString(s string) (Duration, error) { s = strings.Replace(s, "\"", "", -1) s = strings.Replace(s, "'", "", -1) + s = strings.Replace(s, " ", "", -1) // err: 99d55h44m33s123ms - - if v, e := time.ParseDuration(s); e != nil { - return 0, e - } else { - return Duration(v), nil - } + return parseDuration(s) } func (d *Duration) parseString(s string) error { @@ -60,3 +71,193 @@ func (d *Duration) unmarshall(val []byte) error { return nil } } + +// parseDuration parses a duration string. +// code from time.ParseDuration +// A duration string is a possibly signed sequence of +// decimal numbers, each with optional fraction and a unit suffix, +// such as "300ms", "-1.5h" or "2h45m". +// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h", "d". +func parseDuration(s string) (Duration, error) { + // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ + orig := s + var d uint64 + neg := false + + // Consume [-+]? + if s != "" { + c := s[0] + + if c == '-' || c == '+' { + neg = c == '-' + s = s[1:] + } + } + + // Special case: if all that is left is "0", this is zero. + if s == "0" { + return 0, nil + } + + if s == "" { + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + + for s != "" { + var ( + v, f uint64 // integers before, after decimal point + scale float64 = 1 // value = v + f/scale + ) + + var err error + + // The next character must be [0-9.] + if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + + // Consume [0-9]* + pl := len(s) + v, s, err = leadingInt(s) + if err != nil { + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + + pre := pl != len(s) // whether we consumed anything before a period + + // Consume (\.[0-9]*)? + post := false + if s != "" && s[0] == '.' { + s = s[1:] + pl := len(s) + f, scale, s = leadingFraction(s) + post = pl != len(s) + } + + if !pre && !post { + // no digits (e.g. ".s" or "-.s") + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + + // Consume unit. + i := 0 + for ; i < len(s); i++ { + c := s[i] + + if c == '.' || '0' <= c && c <= '9' { + break + } + } + + if i == 0 { + return 0, fmt.Errorf("time: missing unit in duration '%s'" + orig) + } + + u := s[:i] + s = s[i:] + unit, ok := unitMap[u] + + if !ok { + return 0, fmt.Errorf("time: unknown unit '%s' in duration '%s'", u, orig) + } + + if v > 1<<63/unit { + // overflow + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + + v *= unit + + if f > 0 { + // float64 is needed to be nanosecond accurate for fractions of hours. + // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) + v += uint64(float64(f) * (float64(unit) / scale)) + + if v > 1<<63 { + // overflow + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + } + + d += v + + if d > 1<<63 { + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + } + + if neg { + return -Duration(d), nil + } + + if d > 1<<63-1 { + return 0, fmt.Errorf("time: invalid duration '%s'", orig) + } + + return Duration(d), nil +} + +// leadingInt consumes the leading [0-9]* from s. +func leadingInt[bytes []byte | string](s bytes) (x uint64, rem bytes, err error) { + i := 0 + for ; i < len(s); i++ { + c := s[i] + + if c < '0' || c > '9' { + break + } + + if x > 1<<63/10 { + // overflow + return 0, rem, errLeadingInt + } + + x = x*10 + uint64(c) - '0' + + if x > 1<<63 { + // overflow + return 0, rem, errLeadingInt + } + } + + return x, s[i:], nil +} + +// leadingFraction consumes the leading [0-9]* from s. +// It is used only for fractions, so does not return an error on overflow, +// it just stops accumulating precision. +func leadingFraction(s string) (x uint64, scale float64, rem string) { + i := 0 + scale = 1 + overflow := false + + for ; i < len(s); i++ { + c := s[i] + + if c < '0' || c > '9' { + break + } + + if overflow { + continue + } + + if x > (1<<63-1)/10 { + // It's possible for overflow to give a positive number, so take care. + overflow = true + continue + } + + y := x*10 + uint64(c) - '0' + + if y > 1<<63 { + overflow = true + continue + } + + x = y + scale *= 10 + } + + return x, scale, s[i:] +}