From 06df583196d1f5a69be4c5ebe46296a7f0f7e947 Mon Sep 17 00:00:00 2001 From: John Taylor Date: Mon, 17 Jun 2024 07:57:41 -0400 Subject: [PATCH] v1.1.0 * add brief durations (-b cmd line option) * do not allow for duplicate durations * emit better error messages * improve README and example program --- README.md | 50 ++++++++---- cmd/dtdiff/main.go | 26 ++++++- cmd/example/expected-output.txt | 27 ++++--- cmd/example/main.go | 8 ++ dtdiff.go | 134 ++++++++++++++++++++++++++++---- dtdiff_test.go | 26 ++++++- 6 files changed, 227 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b6f6707..473dac5 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ Golang package and command line tool to return or output the difference between * * 2024-06-01T11:22:33.456Z 2. What is the datetime when adding or subtracting a duration? * Duration examples include: -* * 5 minutes 5 seconds -* * 3 weeks 4 days 5 hours -* * 8 months 7 days 6 hours 5 minutes 4 seconds -* * 1 year 2 months 3 days 4 hours 5 minutes 6 second 7 milliseconds 8 microseconds 9 nanoseconds +* * 5 minutes 5 seconds *(or 5m5s)* +* * 3 weeks 4 days 5 hours *(or 3W4D5h)* +* * 8 months 7 days 6 hours 5 minutes 4 seconds *(or 8M7D6h5m4s)* +* * 1 year 2 months 3 days 4 hours 5 minutes 6 second 7 milliseconds 8 microseconds 9 nanoseconds *(or 1Y2M3D4h5m6s7ms8us9ns)* ## Installation @@ -32,12 +32,17 @@ Supported date time formats are listed in: https://go.dev/src/time/format.go **Code Snippet:** ```golang +// import "github.com/jftuga/dtdiff" dt := dtdiff.New("2024-01-01 00:00:00", "2025-12-31 23:59:59") format, _, err := dt.DtDiff() fmt.Println(format) // 2 years 23 hours 59 minutes 59 seconds +// alternatively, use the brief format: +dt.SetBrief(true) +format, _, _ = dt.DtDiff() +fmt.Println(format) // 2Y23h59m59s from := "2024-01-01 00:00:00" -period := "1 day 1 hour 2 minutes 3 seconds" +period := "1 day 1 hour 2 minutes 3 seconds" // can also use: "1D1h2m3s" future, _ := dtdiff.Add(from, period) fmt.Println(future) // 2024-01-02 01:02:03 past, _ := dtdiff.Sub(from, period) @@ -58,19 +63,30 @@ Usage: dtdiff [flags] Globals: - -h, --help help for dtdiff - -n, --nonewline do not output a newline character - -v, --version version for dtdiff + -h, --help help for dtdiff + -n, --nonewline do not output a newline character + -v, --version version for dtdiff Flag Group 1 (mutually exclusive with Flag Group 2): - -e, --end string end date, time, or a datetime - -s, --start string start date, time, or a datetime - -i, --stdin read from STDIN instead of using -s/-e + -b, --brief output in brief format, such as: 1Y2M3D4h5m6s7ms8us9ns + -e, --end string end date, time, or a datetime + -s, --start string start date, time, or a datetime + -i, --stdin read from STDIN instead of using -s/-e Flag Group 2: -A, --add string add: a duration to use with -F, such as '1 day 2 hours 3 seconds' -F, --from string a base date, time or datetime to use with -A or -S -S, --sub string subtract: a duration to use with -F, such as '5 months 4 weeks 3 days' + +Durations: +years months weeks days +hours minutes seconds milliseconds microseconds nanoseconds +example: "1 year 2 months 3 days 4 hours 1 minute 6 seconds" + +Brief Durations: (dates are upper, times are lower) +Y M W D +h m s ms us ns +examples: 1Y2M3W4D5h6m7s8ms9us1ns, "1Y 2M 3W 4D 5h 6m 7s 8ms 9us 1ns" ``` **Note:** The `-i` switch can accept two different types of input: @@ -85,6 +101,10 @@ Flag Group 2: $ dtdiff -s 12:00:00 -e 15:30:45 3 hours 30 minutes 45 seconds +# same input, using brief output +$ dtdiff -s 12:00:00 -e 15:30:45 +3h30m45s + # using AM/PM and not 24-hour times $ dtdiff -s "11:00AM" -e "11:00PM" 12 hours @@ -107,13 +127,17 @@ $ dtdiff -s "$(date -R)" -e "$(date -v+1M -v+30S)" -n # using the cross-platform date program, ending time starting first $ dtdiff -s "$(date)" -e 2020 --4 years 22 weeks 5 days 20 hours 40 minutes 37 seconds +-4 years 24 weeks 1 day 7 hours 21 minutes 53 seconds + +# same input, using brief output +$ dtdiff -s "$(date)" -e 2020 -b +-4Y24W1D7h21m53s # using microsecond formatting $ dtdiff -s 2024-06-07T08:00:00Z -e 2024-06-07T08:00:00.000123Z 123 microseconds -# using millisecond formatting +# using millisecond formatting, adding -b returns: 1m2s345ms $ dtdiff -s 2024-06-07T08:00:00Z -e 2024-06-07T08:01:02.345Z 1 minute 2 seconds 345 milliseconds diff --git a/cmd/dtdiff/main.go b/cmd/dtdiff/main.go index 7cb698b..d997275 100644 --- a/cmd/dtdiff/main.go +++ b/cmd/dtdiff/main.go @@ -26,10 +26,21 @@ Globals: {{FlagUsagesCustom .LocalFlags "nonewline" "help" "version" | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableLocalFlags}} Flag Group 1 (mutually exclusive with Flag Group 2): -{{FlagUsagesCustom .LocalFlags "start" "end" "stdin" | trimTrailingWhitespaces}} +{{FlagUsagesCustom .LocalFlags "start" "end" "stdin" "brief" | trimTrailingWhitespaces}} Flag Group 2: -{{FlagUsagesCustom .LocalFlags "from" "add" "sub" | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} +{{FlagUsagesCustom .LocalFlags "from" "add" "sub" | trimTrailingWhitespaces}} + +Durations: +years months weeks days +hours minutes seconds milliseconds microseconds nanoseconds +example: "1 year 2 months 3 days 4 hours 1 minute 6 seconds" + +Brief Durations: (dates are upper, times are lower) +Y M W D +h m s ms us ns +examples: 1Y2M3W4D5h6m7s8ms9us1ns, "1Y 2M 3W 4D 5h 6m 7s 8ms 9us 1ns" +{{end}}{{if .HasAvailableInheritedFlags}} Global Flags: {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} @@ -46,6 +57,7 @@ var ( sub string noNewline bool readFromStdin bool + brief bool usageMsg string rootCmd = &cobra.Command{ @@ -54,7 +66,7 @@ var ( Short: "dtdiff: output the difference between date, time or duration", Run: func(cmd *cobra.Command, args []string) { if (len(start) > 0 && len(end) > 0) || readFromStdin { - computeStartEnd(start, end) + computeStartEnd(start, end, brief) return } if len(from) > 0 && len(add) > 0 { @@ -112,6 +124,8 @@ func init() { rootCmd.PersistentFlags().StringVarP(&sub, "sub", "S", "", "subtract: a duration to use with -F, such as '5 months 4 weeks 3 days'") rootCmd.PersistentFlags().BoolVarP(&noNewline, "nonewline", "n", false, "do not output a newline character") rootCmd.PersistentFlags().BoolVarP(&readFromStdin, "stdin", "i", false, "read from STDIN instead of using -s/-e") + rootCmd.PersistentFlags().BoolVarP(&brief, "brief", "b", false, "output in brief format, such as: 1Y2M3D4h5m6s7ms8us9ns") + rootCmd.MarkFlagsRequiredTogether("start", "end") rootCmd.MarkFlagsMutuallyExclusive("add", "sub") rootCmd.MarkFlagsMutuallyExclusive("stdin", "start") @@ -125,6 +139,9 @@ func init() { rootCmd.MarkFlagsMutuallyExclusive("add", "end") rootCmd.MarkFlagsMutuallyExclusive("sub", "start") rootCmd.MarkFlagsMutuallyExclusive("sub", "end") + rootCmd.MarkFlagsMutuallyExclusive("brief", "from") + rootCmd.MarkFlagsMutuallyExclusive("brief", "add") + rootCmd.MarkFlagsMutuallyExclusive("brief", "sub") versionTemplate := fmt.Sprintf("%s v%s\n%s\n", dtdiff.PgmName, dtdiff.PgmVersion, dtdiff.PgmUrl) rootCmd.SetVersionTemplate(versionTemplate) @@ -158,12 +175,13 @@ func getInput() (string, string) { } // computeStartEnd used when -s and -e is given -func computeStartEnd(start, end string) { +func computeStartEnd(start, end string, brief bool) { if readFromStdin { start, end = getInput() } dt := dtdiff.New(start, end) + dt.SetBrief(brief) format, _, err := dt.DtDiff() if err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/cmd/example/expected-output.txt b/cmd/example/expected-output.txt index 8063dcc..2aa8dd7 100644 --- a/cmd/example/expected-output.txt +++ b/cmd/example/expected-output.txt @@ -1,18 +1,23 @@ 8:30AM 4:35PM 8h5m0s 8 hours 5 minutes + 8:30AM 4:35PM 8h5m0s 8h5m 11:12:13 14:15:16 3h3m3s 3 hours 3 minutes 3 seconds + 11:12:13 14:15:16 3h3m3s 3h3m3s 2024-03-04T00:00:00Z 2024-06-12T23:59:59Z 2423h59m59s 14 weeks 2 days 23 hours 59 minutes 59 seconds + 2024-03-04T00:00:00Z 2024-06-12T23:59:59Z 2423h59m59s 14W2D23h59m59s 2024-01-01 13:00:00 2024-02-29 13:00:00 1416h0m0s 8 weeks 3 days - 2020-01-01 2024-06-10 11:41:59 38938h41m59s 4 years 23 weeks 1 day 10 hours 41 minutes 59 seconds + 2024-01-01 13:00:00 2024-02-29 13:00:00 1416h0m0s 8W3D + 2020-01-01 2024-06-19 05:53:40 39148h53m40s 4 years 24 weeks 3 days 4 hours 53 minutes 40 seconds + 2020-01-01 2024-06-19 05:53:40 39148h53m40s 4Y24W3D4h53m40s ========================================================================================== -2024-01-01 00:00:00 1 hour 30 minutes 45 seconds 2024-01-01 01:30:45 -0500 EST 2023-12-31 22:29:15 -0500 EST -2024-01-01 00:00:00 12 hours 2024-01-01 12:00:00 -0500 EST 2023-12-31 12:00:00 -0500 EST -2024-01-01 00:00:00 1 day 1 hour 2 minutes 3 seconds 2024-01-02 01:02:03 -0500 EST 2023-12-30 22:57:57 -0500 EST -2024-01-01 00:00:00 5 hours 5 minutes 5 seconds 2024-01-01 05:05:05 -0500 EST 2023-12-31 18:54:55 -0500 EST -2024-01-01 00:00:00 58 seconds 2024-01-01 00:00:58 -0500 EST 2023-12-31 23:59:02 -0500 EST -2024-01-01 00:00:00 1 minute 30 seconds 2024-01-01 00:01:30 -0500 EST 2023-12-31 23:58:30 -0500 EST -2024-01-01 00:00:00 123 microseconds 2024-01-01 00:00:00.000123 -0500 EST 2023-12-31 23:59:59.999877 -0500 EST -2024-01-01 00:00:00 1 minute 2 seconds 345 milliseconds 2024-01-01 00:01:02.345 -0500 EST 2023-12-31 23:58:57.655 -0500 EST -2024-01-01 00:00:00 45 seconds 2024-01-01 00:00:45 -0500 EST 2023-12-31 23:59:15 -0500 EST -2024-01-01 00:00:00 1 minute 5 seconds 2024-01-01 00:01:05 -0500 EST 2023-12-31 23:58:55 -0500 EST +2024-01-01 00:00:00 1 hour 30 minutes 45 seconds 2024-01-01 01:30:45 -0600 CST 2023-12-31 22:29:15 -0600 CST +2024-01-01 00:00:00 12 hours 2024-01-01 12:00:00 -0600 CST 2023-12-31 12:00:00 -0600 CST +2024-01-01 00:00:00 1 day 1 hour 2 minutes 3 seconds 2024-01-02 01:02:03 -0600 CST 2023-12-30 22:57:57 -0600 CST +2024-01-01 00:00:00 5 hours 5 minutes 5 seconds 2024-01-01 05:05:05 -0600 CST 2023-12-31 18:54:55 -0600 CST +2024-01-01 00:00:00 58 seconds 2024-01-01 00:00:58 -0600 CST 2023-12-31 23:59:02 -0600 CST +2024-01-01 00:00:00 1 minute 30 seconds 2024-01-01 00:01:30 -0600 CST 2023-12-31 23:58:30 -0600 CST +2024-01-01 00:00:00 123 microseconds 2024-01-01 00:00:00.000123 -0600 CST 2023-12-31 23:59:59.999877 -0600 CST +2024-01-01 00:00:00 1 minute 2 seconds 345 milliseconds 2024-01-01 00:01:02.345 -0600 CST 2023-12-31 23:58:57.655 -0600 CST +2024-01-01 00:00:00 45 seconds 2024-01-01 00:00:45 -0600 CST 2023-12-31 23:59:15 -0600 CST +2024-01-01 00:00:00 1 minute 5 seconds 2024-01-01 00:01:05 -0600 CST 2023-12-31 23:58:55 -0600 CST diff --git a/cmd/example/main.go b/cmd/example/main.go index 0052d9a..43c56e6 100644 --- a/cmd/example/main.go +++ b/cmd/example/main.go @@ -23,6 +23,14 @@ func main() { continue } fmt.Printf("%21s %21s %13v %55s\n", start, end, duration, format) + + dt.SetBrief(true) + format, duration, err = dt.DtDiff() // you can also use: format, _, err := dt.DtDiff() + if err != nil { + fmt.Fprintln(os.Stderr, err) + continue + } + fmt.Printf("%21s %21s %13v %55s\n", start, end, duration, format) } fmt.Println() diff --git a/dtdiff.go b/dtdiff.go index c8a1a98..1349cb1 100644 --- a/dtdiff.go +++ b/dtdiff.go @@ -1,25 +1,26 @@ package dtdiff import ( - "errors" "fmt" "github.com/golang-module/carbon/v2" "github.com/hako/durafmt" "github.com/jinzhu/now" "regexp" "strconv" + "strings" "time" ) const ( PgmName string = "dtdiff" - PgmVersion string = "1.0.3" + PgmVersion string = "1.1.0" PgmUrl string = "https://github.com/jftuga/dtdiff" ) const ( expanded string = `(\d+)\s(years?|months?|weeks?|days?|hours?|minutes?|seconds?|milliseconds?|microseconds?|nanoseconds?)` wordsOnly string = `\b[a-zA-Z]+\b` + dupMsg string = "Hint: duplicate durations not allowed" ) var carbonFuncs = map[string]interface{}{ @@ -51,10 +52,22 @@ type DtDiff struct { Start string End string Diff time.Duration + Brief bool } func New(start, end string) *DtDiff { - return &DtDiff{Start: start, End: end, Diff: 0} + return &DtDiff{Start: start, End: end, Diff: 0, Brief: false} +} + +// SetBrief toggle brief output when using -s/e +// this returns durations such as "1h2m3s" instead of "1 hour 2 minutes 3 seconds" +func (dt *DtDiff) SetBrief(brief bool) { + dt.Brief = brief +} + +// String return a DtDiff struct in string format +func (dt *DtDiff) String() string { + return fmt.Sprintf("start:%v end:%v duration:%v brief:%v", dt.Start, dt.End, dt.Diff, dt.Brief) } // dur return the time difference and also set dt.Diff @@ -104,6 +117,9 @@ func (dt *DtDiff) DtDiff() (string, time.Duration, error) { } format := dt.format() + if dt.Brief { + format = shrinkPeriod(format) + } return format, duration, nil } @@ -115,7 +131,7 @@ func validatePeriod(period string) error { // fmt.Println("word:", word) _, ok := carbonFuncs[word] if !ok { - return errors.New(fmt.Sprintf("Invalid period: %s", word)) + return fmt.Errorf("[validatePeriod] Invalid period: %s", word) } } return nil @@ -124,12 +140,24 @@ func validatePeriod(period string) error { // calculate Add or Sub a duration of time "period" from the "from" variable // index==0 then Add; index==1 then Sub func calculate(from, period string, index int) (string, error) { + periodMatches := expandedRegexp.FindAllStringSubmatch(period, -1) + if len(periodMatches) == 0 { + // brief format is being used so first expand it to the long format + period, err := expandPeriod(period) + if nil != err { + return "", fmt.Errorf("%v", err) + } + periodMatches = expandedRegexp.FindAllStringSubmatch(period, -1) + if len(periodMatches) == 0 { + return "", fmt.Errorf("[validatePeriod] Invalid duration: %s", period) + } + } + f, err := now.Parse(from) if err != nil { return "", err } - // fmt.Println("\n", from, period, index) to := carbon.CreateFromStdTime(f) if to.Error != nil { return "", to.Error @@ -138,24 +166,104 @@ func calculate(from, period string, index int) (string, error) { if err != nil { return "", err } - results := expandedRegexp.FindAllStringSubmatch(period, -1) - if len(results) == 0 { - return "", errors.New(fmt.Sprintf("Invalid duration: %s", period)) - } - for i := range results { - amount := results[i][1] + + for i := range periodMatches { + amount := periodMatches[i][1] num, err := strconv.Atoi(amount) if err != nil { return "", err } - word := results[i][2] + word := periodMatches[i][2] // to understand this line of code, read: ChatGPT_Explanation.md to = carbonFuncs[word].([2]interface{})[index].(func(carbon.Carbon, int) carbon.Carbon)(to, num) - // fmt.Println("to:", amount, word, to) + // fmt.Printf(" to: %v | %v | %v\n", num, word, to) } return to.ToString(), nil } +// expandPeriod convert a brief style period into a long period +// only allow one replacement per each period +// Ex: 1h2m3s => 1 hour 2 minutes 3 seconds +func expandPeriod(period string) (string, error) { + // a direct string replace will not work because some + // periods have overlapping strings, such as 's' with 'ms, 'us', 'ns' + // therefore convert each period to a unique string first + s := period + s = strings.Replace(s, "ns", "α", 1) + s = strings.Replace(s, "us", "β", 1) + s = strings.Replace(s, "µs", "β", 1) + s = strings.Replace(s, "ms", "γ", 1) + s = strings.Replace(s, "s", "δ", 1) + s = strings.Replace(s, "m", "ε", 1) + s = strings.Replace(s, "h", "ζ", 1) + s = strings.Replace(s, "D", "η", 1) + s = strings.Replace(s, "W", "θ", 1) + s = strings.Replace(s, "M", "ι", 1) + s = strings.Replace(s, "Y", "λ", 1) + + // now convert from the unique string back to the corresponding duration + p := s + p = strings.Replace(p, "α", " nanoseconds ", 1) + p = strings.Replace(p, "β", " microseconds ", 1) + p = strings.Replace(p, "γ", " milliseconds ", 1) + p = strings.Replace(p, "δ", " seconds ", 1) + p = strings.Replace(p, "ε", " minutes ", 1) + p = strings.Replace(p, "ζ", " hours ", 1) + p = strings.Replace(p, "η", " days ", 1) + p = strings.Replace(p, "θ", " weeks ", 1) + p = strings.Replace(p, "ι", " months ", 1) + p = strings.Replace(p, "λ", " years ", 1) + + // ensure each time & period was successfully replaced + // len of Fields should always be even because is part + // of the period is a two element tuple of + // a numeric amount and a duration + words := strings.Fields(p) + if len(words)%2 == 1 { + return "", fmt.Errorf("[expandPeriod] Invalid period: %s. %s", period, dupMsg) + } + + // check that every other element is a number + for i := 0; i < len(words); i += 2 { + _, err := strconv.Atoi(words[i]) + if err != nil { + return "", fmt.Errorf("[expandPeriod] %v. %s", err, dupMsg) + } + } + return p, nil +} + +// shrinkPeriod convert a period into a brief period +// only allow one replacement per each period +// Ex: 1 hour 2 minutes 3 seconds => 1h2m3s +func shrinkPeriod(period string) string { + // plural + period = strings.Replace(period, "nanoseconds", "ns", 1) + period = strings.Replace(period, "microseconds", "us", 1) + period = strings.Replace(period, "milliseconds", "ms", 1) + period = strings.Replace(period, "seconds", "s", 1) + period = strings.Replace(period, "minutes", "m", 1) + period = strings.Replace(period, "hours", "h", 1) + period = strings.Replace(period, "days", "D", 1) + period = strings.Replace(period, "weeks", "W", 1) + period = strings.Replace(period, "months", "M", 1) + period = strings.Replace(period, "years", "Y", 1) + + // singular + period = strings.Replace(period, "nanosecond", "ns", 1) + period = strings.Replace(period, "microsecond", "us", 1) + period = strings.Replace(period, "millisecond", "ms", 1) + period = strings.Replace(period, "second", "s", 1) + period = strings.Replace(period, "minute", "m", 1) + period = strings.Replace(period, "hour", "h", 1) + period = strings.Replace(period, "day", "D", 1) + period = strings.Replace(period, "week", "W", 1) + period = strings.Replace(period, "month", "M", 1) + period = strings.Replace(period, "year", "Y", 1) + + return strings.ReplaceAll(period, " ", "") +} + // Add adds the "period" duration to "from" // this is what is usually called by any consumers func Add(from, period string) (string, error) { diff --git a/dtdiff_test.go b/dtdiff_test.go index 5b6d8a1..bc15e6f 100644 --- a/dtdiff_test.go +++ b/dtdiff_test.go @@ -12,7 +12,7 @@ func testStartEnd(t *testing.T, start, end, correct string) { t.Error(err) } if format != correct { - t.Errorf("computed[%v] != correct[%v]", format, correct) + t.Errorf("[computed: %v] != [correct: %v]", format, correct) } } @@ -22,7 +22,7 @@ func testAddSubContains(t *testing.T, from, period, correctAdd, correctSub strin t.Error(err) } if !strings.Contains(future, correctAdd) { - t.Errorf("computed[%v] does not contain: correct[%v]", future, correctAdd) + t.Errorf("[from: %v] [computed: %v] does not contain: [correct: %v]", from, future, correctAdd) } past, err := Sub(from, period) @@ -30,7 +30,7 @@ func testAddSubContains(t *testing.T, from, period, correctAdd, correctSub strin t.Error(err) } if !strings.Contains(past, correctSub) { - t.Errorf("computed[%v] does not contain: correct[%v]", past, correctSub) + t.Errorf("[from: %v] [computed: %v] does not contain: [correct: %v]", from, past, correctSub) } } @@ -86,71 +86,91 @@ func TestMilliSeconds(t *testing.T) { func TestDurationHours(t *testing.T) { from := "11:00AM" period := "5 hours" + briefPeriod := "5h" correctAdd := " 16:00:00 " correctSub := " 06:00:00 " testAddSubContains(t, from, period, correctAdd, correctSub) + testAddSubContains(t, from, briefPeriod, correctAdd, correctSub) } func TestDurationMillisecondsMicroseconds(t *testing.T) { from := "2024-01-01 00:00:00" period := "1 minute 2 seconds 123 milliseconds 456 microseconds" + briefPeriod := "1m2s123ms456us" correctAdd := "2024-01-01 00:01:02.123456" correctSub := "2023-12-31 23:58:57.876544" testAddSubContains(t, from, period, correctAdd, correctSub) + testAddSubContains(t, from, briefPeriod, correctAdd, correctSub) } func TestDurationHoursMinutesSeconds(t *testing.T) { from := "2024-01-01 00:00:00" period := "5 hours 5 minutes 5 seconds" + briefPeriod := "5h5m5s" correctAdd := "2024-01-01 05:05:05" correctSub := "2023-12-31 18:54:55" testAddSubContains(t, from, period, correctAdd, correctSub) + testAddSubContains(t, from, briefPeriod, correctAdd, correctSub) } func TestDurationYearsMonthsDays(t *testing.T) { from := "2000-01-01" period := "5 years 2 months 10 days" + briefPeriod := "5Y2M10D" correctAdd := "2005-03-11" correctSub := "1994-10-22" testAddSubContains(t, from, period, correctAdd, correctSub) + testAddSubContains(t, from, briefPeriod, correctAdd, correctSub) + } func TestDurationYearsMonthsDaysHoursMinutesSeconds(t *testing.T) { from := "2024-01-01" period := "13 years 8 months 28 days 16 hours 15 minutes 15 seconds" + briefPeriod := "13Y8M28D16h15m15s" correctAdd := "2037-09-29 16:15:15" correctSub := "2010-04-02 07:44:45" testAddSubContains(t, from, period, correctAdd, correctSub) + testAddSubContains(t, from, briefPeriod, correctAdd, correctSub) + } func TestDurationWeeksDays(t *testing.T) { from := "2024-01-01" period := "10 weeks 2 days" + briefPeriod := "10W2D" correctAdd := "2024-03-13" correctSub := "2023-10-21" testAddSubContains(t, from, period, correctAdd, correctSub) + testAddSubContains(t, from, briefPeriod, correctAdd, correctSub) } func TestDurationMonthsWeeksDays(t *testing.T) { from := "2024-06-15" period := "2 months 2 weeks 2 days" + briefPeriod := "2M2W2D" correctAdd := "2024-08-31" correctSub := "2024-03-30" testAddSubContains(t, from, period, correctAdd, correctSub) + testAddSubContains(t, from, briefPeriod, correctAdd, correctSub) } func TestDurationYearsMonthsWeeksDays(t *testing.T) { from := "2031-07-12" period := "2 years 2 months 2 weeks 2 days" + briefPeriod := "2Y2M2W2D" correctAdd := "2033-09-28" correctSub := "2029-04-26" testAddSubContains(t, from, period, correctAdd, correctSub) + testAddSubContains(t, from, briefPeriod, correctAdd, correctSub) } func TestDurationNanoseconds(t *testing.T) { from := "2031-07-11 05:00:00" period := "987654321 nanoseconds" + briefPeriod := "987654321ns" correctAdd := "2031-07-11 05:00:00.987654321" correctSub := "2031-07-11 04:59:59.012345679" testAddSubContains(t, from, period, correctAdd, correctSub) + testAddSubContains(t, from, briefPeriod, correctAdd, correctSub) }