diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..425fa53
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,33 @@
+on:
+ workflow_dispatch:
+ push:
+ tags:
+ - "*"
+
+permissions:
+ contents: write
+
+jobs:
+ build:
+ name: GoReleaser Build
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out code into the Go module directory
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set Up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.x"
+ id: go
+
+ - name: run GoReleaser
+ uses: goreleaser/goreleaser-action@v6
+ env:
+ HOMEBREW_TOKEN: ${{ secrets.HOMEBREW_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ args: release --clean
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..ae121cc
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,87 @@
+version: 2
+
+before:
+ hooks:
+ - go mod tidy
+
+builds:
+ - id: dtmate
+ binary: dtmate
+ dir: ./cmd/dtmate
+ ldflags:
+ - -extldflags "-static" -s -w -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}}
+ env:
+ - CGO_ENABLED=0
+ goos:
+ - linux
+ - freebsd
+ - darwin
+ goarch:
+ - amd64
+ - arm64
+ - arm
+ - ppc64le
+ goarm:
+ - "7"
+ ignore:
+ - goos: freebsd
+ goarch: arm64
+ - goos: freebsd
+ goarch: arm
+ - goos: freebsd
+ goarch: ppc64le
+ - goos: darwin
+ goarch: arm
+ - goos: darwin
+ goarch: ppc64le
+
+ - id: dtmate-win
+ binary: dtmate
+ dir: ./cmd/dtmate
+ ldflags:
+ - -extldflags "-static" -s -w -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}}
+ env:
+ - CGO_ENABLED=0
+ goos:
+ - windows
+ goarch:
+ - amd64
+ hooks:
+ post:
+ - upx -9 "{{ .Path }}"
+
+archives:
+ - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
+ format: tar.xz
+ format_overrides:
+ - goos: windows
+ format: zip
+ wrap_in_directory: true
+ files:
+ - LICENSE
+ - README.md
+
+checksum:
+ name_template: "{{ .ProjectName }}_{{ .Version }}--checksums.txt"
+release:
+ draft: false
+changelog:
+ sort: asc
+ filters:
+ exclude:
+ - "^docs:"
+ - "^test:"
+
+brews:
+ - name: dtmate
+ repository:
+ owner: jftuga
+ name: homebrew-tap
+ token: "{{ .Env.HOMEBREW_TOKEN }}"
+ commit_author:
+ name: jftuga
+ email: jftuga@users.noreply.github.com
+ homepage: https://github.com/jftuga/DateTimeMate
+ description: "dtmate: output the difference between date, time or duration"
+ test: system "#{bin}/dtmate -v"
+ install: bin.install "dtmate"
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/DateTimeMate.iml b/.idea/DateTimeMate.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/DateTimeMate.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..3a7375b
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..6d0aeaf
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DateTimeMate.go b/DateTimeMate.go
new file mode 100644
index 0000000..c658bc1
--- /dev/null
+++ b/DateTimeMate.go
@@ -0,0 +1,28 @@
+package DateTimeMate
+
+import (
+ "github.com/golang-module/carbon/v2"
+ "strings"
+)
+
+const (
+ ModName string = "DateTimeMate"
+ ModVersion string = "1.0.0"
+ ModUrl string = "https://github.com/jftuga/DateTimeMate"
+)
+
+// convertRelativeDateToActual converts "yesterday", "today", "tomorrow"
+// into actual dates; yesterday and tomorrow are -/+ 24 hours of current time
+func convertRelativeDateToActual(from string) string {
+ switch strings.ToLower(from) {
+ case "now":
+ return carbon.Now().String()
+ case "today":
+ return carbon.Now().String()
+ case "yesterday":
+ return carbon.Yesterday().String()
+ case "tomorrow":
+ return carbon.Tomorrow().String()
+ }
+ return from
+}
diff --git a/DateTimeMate_test.go b/DateTimeMate_test.go
new file mode 100644
index 0000000..608a700
--- /dev/null
+++ b/DateTimeMate_test.go
@@ -0,0 +1,361 @@
+package DateTimeMate
+
+import (
+ "fmt"
+ "github.com/golang-module/carbon/v2"
+ "strings"
+ "testing"
+)
+
+func testStartEnd(t *testing.T, start, end string, brief bool, correct string) {
+ diff := NewDiff(
+ DiffWithStart(start),
+ DiffWithEnd(end),
+ DiffWithBrief(brief))
+ result, _, err := diff.CalculateDiff()
+ if err != nil {
+ t.Error(err)
+ }
+ if result != correct {
+ t.Errorf("[computed: %v] != [correct: %v]", result, correct)
+ }
+}
+
+func testAddSubContains(t *testing.T, from, period, correctAdd, correctSub string) {
+ dur := NewDur(
+ DurWithFrom(from),
+ DurWithDur(period))
+ future, err := dur.Add()
+ if err != nil {
+ t.Error(err)
+ }
+ if !strings.Contains(future[0], correctAdd) {
+ t.Errorf("[from: %v] [computed: %v] does not contain: [correct: %v]", from, future, correctAdd)
+ }
+
+ past, err := dur.Sub()
+ if err != nil {
+ t.Error(err)
+ }
+ if !strings.Contains(past[0], correctSub) {
+ t.Errorf("[from: %v] [computed: %v] does not contain: [correct: %v]", from, past, correctSub)
+ }
+}
+
+func testAddSubOutputFormat(t *testing.T, from, period, outputFormat, correctAdd, correctSub string) {
+ dur := NewDur(
+ DurWithFrom(from),
+ DurWithDur(period),
+ DurWithOutputFormat(outputFormat))
+ future, err := dur.Add()
+ if err != nil {
+ t.Error(err)
+ }
+ if !strings.Contains(future[0], correctAdd) {
+ t.Errorf("\n[from: %v]\n[period: %v]\n[computed: %v] does not contain:\n[correct: %v]", from, period, future[0], correctAdd)
+ }
+ return
+
+ past, err := dur.Sub()
+ if err != nil {
+ t.Error(err)
+ }
+ if !strings.Contains(past[0], correctSub) {
+ t.Errorf("[from: %v] [computed: %v] does not contain: [correct: %v]", from, past, correctSub)
+ }
+}
+
+func testAddSubWithRepeat(t *testing.T, from, period string, correctAdd, correctSub []string, repeat int) {
+ dur := NewDur(
+ DurWithFrom(from),
+ DurWithDur(period),
+ DurWithRepeat(repeat))
+ future, err := dur.Add()
+ if err != nil {
+ t.Error(err)
+ }
+ for i := range len(future) {
+ if !strings.Contains(future[i], correctAdd[i]) {
+ t.Errorf("[from: %v] [computed: %v] does not contain: [correct: %v]", from, future[i], correctAdd[i])
+ }
+ }
+
+ past, err := dur.Sub()
+ if err != nil {
+ t.Error(err)
+ }
+ for j := range len(past) {
+ if !strings.Contains(past[j], correctSub[j]) {
+ t.Errorf("[from: %v] [computed: %v] does not contain: [correct: %v]", from, past[j], correctSub[j])
+ }
+ }
+}
+
+func testAddUntil(t *testing.T, from, until, periodFuture string, correctAdd []string) {
+ durFuture := NewDur(
+ DurWithFrom(from),
+ DurWithDur(periodFuture),
+ DurWithUntil(until))
+ future, err := durFuture.Add()
+ if err != nil {
+ t.Error(err)
+ }
+
+ for i := range len(future) {
+ if !strings.Contains(future[i], correctAdd[i]) {
+ t.Errorf("[from: %v] [computed: %v] does not contain: [correct: %v]", from, future[i], correctAdd[i])
+ break
+ }
+ }
+}
+
+func testSubUntil(t *testing.T, from, until, periodPast string, correctSub []string) {
+ durPast := NewDur(
+ DurWithFrom(from),
+ DurWithDur(periodPast),
+ DurWithUntil(until))
+ past, err := durPast.Sub()
+ if err != nil {
+ t.Error(err)
+ }
+ for i := range len(past) {
+ if !strings.Contains(past[i], correctSub[i]) {
+ t.Errorf("[from: %v] [computed: %v] does not contain: [correct: %v]", from, past[i], correctSub[i])
+ }
+ }
+}
+
+func TestTwoTimesSameDay(t *testing.T) {
+ start := "12:00:00"
+ end := "15:30:45"
+ correct := "3 hours 30 minutes 45 seconds"
+ correctBrief := "3h30m45s"
+ testStartEnd(t, start, end, false, correct)
+ testStartEnd(t, start, end, true, correctBrief)
+}
+
+func TestAmPm(t *testing.T) {
+ start := "11:00AM"
+ end := "11:00PM"
+ correct := "12 hours"
+ correctBrief := "12h"
+ testStartEnd(t, start, end, false, correct)
+ testStartEnd(t, start, end, true, correctBrief)
+}
+
+func TestIso8601(t *testing.T) {
+ start := "2024-06-07T08:00:00Z"
+ end := "2024-07-08T09:02:03Z"
+ correct := "4 weeks 3 days 1 hour 2 minutes 3 seconds"
+ correctBrief := "4W3D1h2m3s"
+ testStartEnd(t, start, end, false, correct)
+ testStartEnd(t, start, end, true, correctBrief)
+}
+
+func TestTimeZoneOffset(t *testing.T) {
+ start := "2024-06-07T08:00:00Z"
+ end := "2024-06-07T08:05:05-05:00"
+ correct := "5 hours 5 minutes 5 seconds"
+ correctBrief := "5h5m5s"
+ testStartEnd(t, start, end, false, correct)
+ testStartEnd(t, start, end, true, correctBrief)
+}
+
+func TestIncludeSpaces(t *testing.T) {
+ start := "2024-06-07 08:01:02"
+ end := "2024-06-07 08:02"
+ correct := "58 seconds"
+ correctBrief := "58s"
+ testStartEnd(t, start, end, false, correct)
+ testStartEnd(t, start, end, true, correctBrief)
+}
+
+func TestMicroSeconds(t *testing.T) {
+ start := "2024-06-07T08:00:00Z"
+ end := "2024-06-07T08:00:00.000123Z"
+ correct := "123 microseconds"
+ correctBrief := "123us"
+ testStartEnd(t, start, end, false, correct)
+ testStartEnd(t, start, end, true, correctBrief)
+}
+
+func TestMilliSeconds(t *testing.T) {
+ start := "2024-06-07T08:00:00Z"
+ end := "2024-06-07T08:01:02.345Z"
+ correct := "1 minute 2 seconds 345 milliseconds"
+ correctBrief := "1m2s345ms"
+ testStartEnd(t, start, end, false, correct)
+ testStartEnd(t, start, end, true, correctBrief)
+}
+
+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)
+ ofmt := " %H:%M:%S "
+ testAddSubOutputFormat(t, from, period, ofmt, 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)
+ correctAdd = correctAdd[:19]
+ correctSub = correctSub[:19]
+ ofmt := "%Y-%m-%d %H:%M:%S"
+ testAddSubOutputFormat(t, from, period, ofmt, 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)
+ ofmt := "%Y-%m-%d %H:%M:%S"
+ testAddSubOutputFormat(t, from, period, ofmt, 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)
+ ofmt := "%Y-%m-%d"
+ testAddSubOutputFormat(t, from, period, ofmt, 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)
+ ofmt := "%Y-%m-%d %H:%M:%S"
+ testAddSubOutputFormat(t, from, period, ofmt, 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)
+ ofmt := "%Y-%m-%d"
+ testAddSubOutputFormat(t, from, period, ofmt, 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)
+ ofmt := "%Y-%m-%d"
+ testAddSubOutputFormat(t, from, period, ofmt, 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)
+ ofmt := "%Y-%m-%d"
+ testAddSubOutputFormat(t, from, period, ofmt, 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)
+ correctAdd = correctAdd[:19]
+ correctSub = correctSub[:19]
+ ofmt := "%Y-%m-%d %H:%M:%S"
+ testAddSubOutputFormat(t, from, period, ofmt, correctAdd, correctSub)
+}
+
+func TestWithRepeat(t *testing.T) {
+ from := "2024-06-28T04:25:41Z"
+ period := "1M1W1h1m2s"
+ repeat := 3
+ allCorrectAdd := []string{"2024-08-04 05:26:43", "2024-09-11 06:27:45", "2024-10-18 07:28:47"}
+ allCorrectSub := []string{"2024-05-21 03:24:39", "2024-04-14 02:23:37", "2024-03-07 01:22:35"}
+ testAddSubWithRepeat(t, from, period, allCorrectAdd, allCorrectSub, repeat)
+}
+
+func TestAddUntil(t *testing.T) {
+ from := "2024-06-28T04:25:41Z"
+ period := "1M1W1h1m2s"
+ until := "2024-10-18 07:28:47"
+ allCorrectAdd := []string{"2024-08-04 05:26:43", "2024-09-11 06:27:45", "2024-10-18 07:28:47"}
+ testAddUntil(t, from, until, period, allCorrectAdd)
+}
+
+func TestSubUntil(t *testing.T) {
+ from := "2024-10-18 07:28:47"
+ period := "1M1W1h1m2s"
+ until := "2024-05-28T04:25:41Z"
+ allCorrectSub := []string{"2024-09-11 06:27:45", "2024-08-04 05:26:43", "2024-06-27 04:25:41"}
+ testSubUntil(t, from, until, period, allCorrectSub)
+}
+
+func TestRelativeStartEnd(t *testing.T) {
+ start := "yesterday"
+ end := "Today"
+ correct := "1 day"
+ testStartEnd(t, start, end, false, correct)
+
+ start = "Yesterday"
+ end = "tomorrow"
+ correct = "2 days"
+ testStartEnd(t, start, end, false, correct)
+
+ start = "now"
+ end = "today"
+ correct = "0 seconds"
+ testStartEnd(t, start, end, false, correct)
+
+ start = "today"
+ end = "tomorrow"
+ correct = "1 day"
+ testStartEnd(t, start, end, false, correct)
+}
+
+func TestRelativeUntil(t *testing.T) {
+ from := carbon.Now().StartOfDay().ToDateTimeString()
+ period := "7h59m1s"
+ until := "tomorrow"
+ allCorrectAdd := []string{"", "", "", ""}
+ allCorrectAdd[0] = fmt.Sprintf("%s", strings.Replace(from, "00:00:00", "07:59:01", 1))
+ allCorrectAdd[1] = fmt.Sprintf("%s", strings.Replace(from, "00:00:00", "15:58:02", 1))
+ allCorrectAdd[2] = fmt.Sprintf("%s", strings.Replace(from, "00:00:00", "23:57:03", 1))
+ allCorrectAdd[3] = fmt.Sprintf("%s", strings.Replace(from, "00:00:00", "07:56:04", 1))
+ allCorrectAdd[3] = fmt.Sprintf("%s", strings.Replace(allCorrectAdd[3], carbon.Now().ToDateString(), carbon.Tomorrow().ToDateString(), 1))
+ testAddUntil(t, from, until, period, allCorrectAdd)
+}
diff --git a/README.md b/README.md
index 275b827..928f9f4 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,260 @@
# DateTimeMate
Golang package and CLI to compute the difference between date, time or duration
+
+The command-line program, `dtmate` *(along with the golang package)* allows you to answer three types of questions:
+
+
+1. What is the duration between two different dates and/or times?
+
+`dtmate diff "2024-06-01 11:22:33" "2024-07-19 21:07:19"`
+* answer: `6 weeks 6 days 9 hours 44 minutes 46 seconds`
+* answer with the `-b` option: `6W6D9h44m46s`
+* start and end can be in various formats, such as:
+* * `11:22:33`, `2024-06-01`, `"2024-06-01 11:22:33"`, `2024-06-01T11:22:33.456Z`
+
+
+
+2. What is the datetime when adding or subtracting a duration?
+
+`dtmate dur "2024-06-01 11:22:33" 6W6D9h44m46s -a`
+* answer: `2024-04-14 01:37:47 -0400 EDT`
+* answer with the `-f "%Y-%m-%d %H:%M:%S"` option: `2024-04-14 01:37:47`
+* Duration examples include:
+* * `5 minutes 5 seconds or 5m5s`
+* * `3 weeks 4 days 5 hours or 3W4D5h`
+* * `1 year 2 months 3 days 4 hours 5 minutes 6 second 7 milliseconds 8 microseconds 9 nanoseconds or 1Y2M3D4h5m6s7ms8us9ns`
+
+
+
+3. Similar to previous question, but repeats a period multiple times or until a certain date/time is encountered.
+
+* adding dates, repeat twice: `dtmate dur "2024-06-01 12:00:00" 1h5m10s -r 2 -a`
+* subtracting until a date is exceeded: `dtmate dur "12:00:00" 1h5m10s -u "09:48" -s`
+
+
+## Installation
+
+* Library: `go get -u github.com/jftuga/DateTimeMate`
+* Command line tool: `go install github.com/jftuga/DateTimeMate/cmd/dtmate@latest`
+* * Binaries for all platforms are provided in the [releases](https://github.com/jftuga/DateTimeMate/releases) section.
+* Homebrew (MacOS / Linux):
+* * `brew tap jftuga/homebrew-tap; brew update; brew install jftuga/tap/DateTimeMate`
+
+## Library Usage
+
+Example 1 - duration between two dates
+
+Supported date time formats are listed in: https://go.dev/src/time/format.go
+
+```golang
+import "github.com/jftuga/DateTimeMate"
+
+// example 1 - duration between two dates
+start := "2024-06-01"
+end := "2024-08-05 00:01:02"
+brief := true
+diff := DateTimeMate.NewDiff(DateTimeMate.DiffWithStart(start), DateTimeMate.DiffWithEnd(end),
+ DateTimeMate.DiffWithBrief(brief))
+result, duration, err := diff.CalculateDiff()
+if err != nil { ... }
+fmt.Println(result, duration) // 9W2D1m2s 1560h1m2s
+```
+
+
+
+Example 2 - add a duration
+
+```go
+// example 2 - add a duration and repeat it until the "until" date is exceeded
+from := "2024-06-01"
+d := "1 year 7 days 6 hours 5 minutes"
+until := "2027-06-22 18:15:11"
+ofmt := "%Y%m%d.%H%M%S"
+dur := DateTimeMate.NewDur(DateTimeMate.DurWithFrom(from), DateTimeMate.DurWithDur(d),
+ DateTimeMate.DurWithRepeat(0), DateTimeMate.DurWithUntil(until),
+ DateTimeMate.DurWithOutputFormat(ofmt))
+add, err := dur.Add()
+if err != nil { ... }
+fmt.Println(add) // [20250608.060500 20260615.121000 20270622.181500]
+```
+
+
+See also the [example](cmd/example/main.go) program.
+
+
+## Command Line Usage
+
+
+Show
+
+```
+dtmate: output the difference between date, time or duration
+
+Usage:
+ dtmate [command]
+
+Available Commands:
+ diff Output the difference between two date/times
+ dur Output a date/time when given a starting date/time and duration
+ help Help about any command
+
+Flags:
+ -h, --help help for dtmate
+ -n, --nonewline do not output a newline character
+ -t, --toggle Help message for toggle
+ -v, --version version for dtmate
+
+Use "dtmate [command] --help" for more information about a command.
+
+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 always uppercase, times are always lowercase)
+Y M W D
+h m s ms us ns
+examples: 1Y2M3W4D5h6m7s8ms9us1ns, '1Y 2M 3W 4D 5h 6m 7s 8ms 9us 1ns'
+
+Relative Date Shortcuts:
+now
+today (returns same value as now)
+yesterday (exactly 24 hours ahead of the current time)
+tomorrow (exactly 24 hours behind the current time)
+example: dtmate dur today 7h10m -a -u tomorrow
+```
+
+
+
+**Note:** The `-i` switch can accept two different types of input:
+
+1. one line with start and end separated by a comma
+2. two lines with start on the first line and end on the second line
+
+**Note:** The `-n` switch along with `-R` will emit a comma-delimited output
+
+## Command Line Examples
+
+
+Show
+
+```shell
+# difference between two times on the same day
+$ dtmate diff 12:00:00 15:30:45
+3 hours 30 minutes 45 seconds
+
+# same input, using brief output
+$ dtmate diff 12:00:00 15:30:45 -b
+3h30m45s
+
+# using AM/PM and not 24-hour times
+$ dtmate diff "11:00AM" "11:00PM"
+12 hours
+
+# using ISO-8601 dates
+$ dtmate diff 2024-06-07T08:00:00Z 2024-06-08T09:02:03Z
+1 day 1 hour 2 minutes 3 seconds
+
+# using timezone offset
+$ dtmate diff 2024-06-07T08:00:00Z 2024-06-07T08:05:05-05:00
+5 hours 5 minutes 5 seconds
+
+# using a format which includes spaces
+$ dtmate diff "2024-06-07 08:01:02" "2024-06-07 08:02"
+58 seconds
+
+# using the built-in MacOS date program and do not include a newline character
+$ dtmate diff "$(date -R)" "$(date -v+1M -v+30S)" -n
+1 minute 30 seconds%
+
+# using the cross-platform date program, ending time starting first
+$ dtmate diff "$(date)" 2020
+-4 years 24 weeks 1 day 7 hours 21 minutes 53 seconds
+
+# same input, using brief output
+$ dtmate diff "$(date)" 2020 -b
+-4Y24W1D7h21m53s
+
+# using microsecond formatting
+$ dtmate diff 2024-06-07T08:00:00Z 2024-06-07T08:00:00.000123Z
+123 microseconds
+
+# using millisecond formatting, adding -b returns: 1m2s345ms
+$ dtmate diff 2024-06-07T08:00:00Z 2024-06-07T08:01:02.345Z
+1 minute 2 seconds 345 milliseconds
+
+# read from STDIN in CSV format and do not include a newline character
+$ dtmate diff -i -n
+15:16:15,15:17
+45 seconds%
+
+# same as above, include newline character
+$ echo 15:16:15,15:17 | dtmate -i
+45 seconds
+
+# read from STDIN with start on first line and end on second line
+$ printf "15:16:15\n15:17:20" | dtmate diff -i
+1 minute 5 seconds
+
+# add time
+# can also use "years", "months", "weeks", "days"
+$ dtmate dur 2024-01-01 "1 hour 30 minutes 45 seconds" -a
+2024-01-01 01:30:45 -0500 EST
+
+# subtract time
+# can also use "milliseconds", "microseconds"
+$ dtmate dur "2024-01-02 01:02:03" "1 day 1 hour 2 minutes 3 seconds" -s
+2024-01-01 00:00:00 -0500 EST
+
+# output multiple occurrences: add 5 weeks, for 3 intervals
+$ dtmate dur "2024-01-02" "5W" -r 3 -a
+2024-02-06 00:00:00 -0500 EST
+2024-03-12 00:00:00 -0400 EDT
+2024-04-16 00:00:00 -0400 EDT
+
+# repeat until a certain datetime is encountered: subtract 5 minutes until 15:00
+$ dtmate dur 15:20 5m -u 15:00 -s
+2024-06-30 15:15:00 -0400 EDT
+2024-06-30 15:10:00 -0400 EDT
+2024-06-30 15:05:00 -0400 EDT
+2024-06-30 15:00:00 -0400 EDT
+
+# use relative date until tomorrow
+$ dtmate dur today 7h10m -u tomorrow -a
+2024-07-03 14:29:28 -0400 EDT
+2024-07-03 21:39:28 -0400 EDT
+2024-07-04 04:49:28 -0400 EDT
+
+# use relative start date with brief output
+$ dtmate diff today 2024-07-07 -b
+3D16h38m47s
+
+# set the output format
+$ dtmate dur "2024-07-01 12:00:00" 1W2D3h4m5s -a -f "%Y%m%d.%H%M%S"
+20240710.150405
+```
+
+
+## LICENSE
+
+[MIT LICENSE](LICENSE)
+
+## Acknowledgements
+
+
+Imported Modules
+
+* carbon - https://github.com/golang-module/carbon
+* cobra - https://github.com/spf13/cobra
+* durafmt - https://github.com/hako/durafmt
+* parsetime - https://github.com/tkuchiki/parsetime
+* strftime - https://github.com/lestrrat-go/strftime
+
+
+
+## Disclosure Notification
+
+This program is my own original idea and was completely developed
+on my own personal time, for my own personal benefit, and on my
+personally owned equipment.
+
diff --git a/cmd/dtmate/cmd/diff.go b/cmd/dtmate/cmd/diff.go
new file mode 100644
index 0000000..8e9c625
--- /dev/null
+++ b/cmd/dtmate/cmd/diff.go
@@ -0,0 +1,82 @@
+package cmd
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "github.com/jftuga/DateTimeMate"
+ "os"
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+// diffCmd represents the diff command
+var diffCmd = &cobra.Command{
+ Use: "diff [start] [end]",
+ Short: "Output the difference between two date/times",
+ Args: func(cmd *cobra.Command, args []string) error {
+ if optDiffReadFromStdin {
+ if len(args) == 0 {
+ return nil
+ } else {
+ return errors.New("invalid number of arguments")
+ }
+ }
+ if len(args) != 2 {
+ return errors.New("requires two arguments: start, end date/times")
+ }
+ return nil
+ },
+ Run: func(cmd *cobra.Command, args []string) {
+ if optDiffReadFromStdin {
+ start, end := getInput()
+ outputDiff(start, end, optDiffBrief)
+ return
+ }
+ outputDiff(args[0], args[1], optDiffBrief)
+ },
+}
+
+var optDiffBrief bool
+var optDiffReadFromStdin bool
+
+func init() {
+ rootCmd.AddCommand(diffCmd)
+ diffCmd.Flags().BoolVarP(&optDiffBrief, "brief", "b", false, "output in brief format, such as: 1Y2M3D4h5m6s7ms8us9ns")
+ diffCmd.Flags().BoolVarP(&optDiffReadFromStdin, "stdin", "i", false, "read from STDIN instead of using -s/-e")
+}
+
+// either read one line containing a comma, then split start and end on this
+// or read two lines with start on line one and end on line two
+func getInput() (string, string) {
+ input := bufio.NewScanner(os.Stdin)
+ input.Scan()
+ line := input.Text()
+ if strings.Contains(line, ",") {
+ split := strings.Split(line, ",")
+ if len(split) != 2 {
+ fmt.Fprintf(os.Stderr, "invalid stdin input: %s\n", line)
+ os.Exit(1)
+ }
+ return split[0], split[1]
+ }
+ input.Scan()
+ end := input.Text()
+ return line, end
+}
+
+// outputDiff compute the duration between two dates, times, and/or date/times
+func outputDiff(start, end string, brief bool) {
+ diff := DateTimeMate.NewDiff(DateTimeMate.DiffWithStart(start), DateTimeMate.DiffWithEnd(end), DateTimeMate.DiffWithBrief(brief))
+ result, _, err := diff.CalculateDiff()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ if optRootNoNewline {
+ fmt.Print(result)
+ } else {
+ fmt.Println(result)
+ }
+}
diff --git a/cmd/dtmate/cmd/dur.go b/cmd/dtmate/cmd/dur.go
new file mode 100644
index 0000000..dbbd63f
--- /dev/null
+++ b/cmd/dtmate/cmd/dur.go
@@ -0,0 +1,72 @@
+package cmd
+
+import (
+ "fmt"
+ "github.com/jftuga/DateTimeMate"
+ "os"
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+// durCmd represents the dur command
+var durCmd = &cobra.Command{
+ Use: "dur [from] [duration]",
+ Short: "Output a date/time when given a starting date/time and duration",
+ Args: cobra.MatchAll(cobra.ExactArgs(2)),
+ Run: func(cmd *cobra.Command, args []string) {
+ outputDur(args[0], args[1], optDurUntil, optDurFormat, optDurRepeat)
+ },
+}
+
+var (
+ //optDurFrom string
+ optDurAdd bool
+ optDurSub bool
+ optDurUntil string
+ optDurFormat string
+ optDurRepeat int
+)
+
+func init() {
+ rootCmd.AddCommand(durCmd)
+ durCmd.Flags().BoolVarP(&optDurAdd, "add", "a", false, "add: a duration to use with -f, such as '1D2h3s' or '1 day 2 hours 3 seconds'")
+ durCmd.Flags().BoolVarP(&optDurSub, "sub", "s", false, "subtract: a duration to use with -f, such as '5 months 4 weeks 3 days'")
+ durCmd.Flags().StringVarP(&optDurUntil, "until", "u", "", "repeat duration until this date/time is exceeded")
+ durCmd.Flags().StringVarP(&optDurFormat, "format", "f", "", "output results with strftime formatting")
+ durCmd.Flags().IntVarP(&optDurRepeat, "repeat", "r", 0, "repeat the -a or -s duration this number of times (mutually exclusive with -u)")
+ durCmd.MarkFlagsOneRequired("add", "sub")
+ durCmd.MarkFlagsMutuallyExclusive("add", "sub")
+ durCmd.MarkFlagsMutuallyExclusive("repeat", "until")
+}
+
+func outputDur(from, duration, until, format string, repeat int) {
+ dur := DateTimeMate.NewDur(
+ DateTimeMate.DurWithFrom(from),
+ DateTimeMate.DurWithDur(duration),
+ DateTimeMate.DurWithUntil(until),
+ DateTimeMate.DurWithRepeat(repeat),
+ DateTimeMate.DurWithOutputFormat(format))
+
+ var allResults []string
+ var err error
+ if optDurAdd {
+ allResults, err = dur.Add()
+ } else {
+ allResults, err = dur.Sub()
+ }
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ delim := "\n"
+ if optRootNoNewline {
+ delim = ","
+ }
+ output := strings.Join(allResults, delim)
+ fmt.Print(output)
+ if !optRootNoNewline {
+ fmt.Println()
+ }
+}
diff --git a/cmd/dtmate/cmd/root.go b/cmd/dtmate/cmd/root.go
new file mode 100644
index 0000000..8815c9a
--- /dev/null
+++ b/cmd/dtmate/cmd/root.go
@@ -0,0 +1,54 @@
+package cmd
+
+import (
+ "fmt"
+ DateTimeMate "github.com/jftuga/DateTimeMate"
+ "github.com/spf13/cobra"
+ "os"
+)
+
+const extendedHelp string = `
+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 always uppercase, times are always lowercase)
+Y M W D
+h m s ms us ns
+examples: 1Y2M3W4D5h6m7s8ms9us1ns, '1Y 2M 3W 4D 5h 6m 7s 8ms 9us 1ns'
+
+Relative Date Shortcuts:
+now
+today (returns same value as now)
+yesterday (exactly 24 hours ahead of the current time)
+tomorrow (exactly 24 hours behind the current time)
+example: dtmate dur today 7h10m -a -u tomorrow
+`
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+ Use: "dtmate",
+ Short: "dtmate: output the difference between date, time or duration",
+ Version: DateTimeMate.ModVersion,
+}
+
+var optRootNoNewline bool
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+ err := rootCmd.Execute()
+ if err != nil {
+ os.Exit(1)
+ }
+}
+
+func init() {
+ rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+ rootCmd.PersistentFlags().BoolVarP(&optRootNoNewline, "nonewline", "n", false, "do not output a newline character")
+ versionTemplate := fmt.Sprintf("dtmate version %s\n%s\n", DateTimeMate.ModVersion, DateTimeMate.ModUrl)
+ rootCmd.SetVersionTemplate(versionTemplate)
+ rootCmd.CompletionOptions.DisableDefaultCmd = true
+ rootCmd.SetUsageTemplate(rootCmd.UsageTemplate() + extendedHelp)
+}
diff --git a/cmd/dtmate/main.go b/cmd/dtmate/main.go
new file mode 100644
index 0000000..9f8da08
--- /dev/null
+++ b/cmd/dtmate/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "github.com/jftuga/DateTimeMate/cmd/dtmate/cmd"
+
+func main() {
+ cmd.Execute()
+}
diff --git a/cmd/example/main.go b/cmd/example/main.go
new file mode 100644
index 0000000..205084a
--- /dev/null
+++ b/cmd/example/main.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "fmt"
+ "github.com/jftuga/DateTimeMate"
+ "os"
+)
+
+func main() {
+ fmt.Println()
+
+ start := "2024-06-01"
+ end := "2024-08-05 00:01:02"
+ brief := true
+ diff := DateTimeMate.NewDiff(
+ DateTimeMate.DiffWithStart(start),
+ DateTimeMate.DiffWithEnd(end),
+ DateTimeMate.DiffWithBrief(brief))
+ fmt.Println(diff)
+ fmt.Println("===================================================")
+
+ result, duration, err := diff.CalculateDiff()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println("duration:", result, "=>", duration)
+ fmt.Println("===================================================")
+
+ from := "2024-06-01"
+ d := "1 year 7 days 6 hours 5 minutes"
+ until := "2027-06-22 18:15:11"
+ ofmt := "%Y%m%d.%H%M%S"
+ dur := DateTimeMate.NewDur(
+ DateTimeMate.DurWithFrom(from),
+ DateTimeMate.DurWithDur(d),
+ DateTimeMate.DurWithRepeat(0),
+ DateTimeMate.DurWithUntil(until),
+ DateTimeMate.DurWithOutputFormat(ofmt))
+ fmt.Println("duration:", dur)
+ fmt.Println("===================================================")
+
+ add1, err := dur.Add()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println("Add1: ", add1)
+ fmt.Println("===================================================")
+ dur.Until = ""
+ dur.Repeat = 3
+
+ add2, err := dur.Add()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println("Add2: ", add2)
+ fmt.Println("===================================================")
+
+ until = "2020-05-02 23:41:00"
+ ofmt = "%v %T"
+ dur = DateTimeMate.NewDur(
+ DateTimeMate.DurWithFrom(from),
+ DateTimeMate.DurWithDur(d),
+ DateTimeMate.DurWithUntil(until),
+ DateTimeMate.DurWithOutputFormat(ofmt))
+ sub1, err := dur.Sub()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println("Sub1: ", sub1)
+}
diff --git a/diff.go b/diff.go
new file mode 100644
index 0000000..fd225a5
--- /dev/null
+++ b/diff.go
@@ -0,0 +1,91 @@
+package DateTimeMate
+
+import (
+ "fmt"
+ "github.com/golang-module/carbon/v2"
+ "github.com/hako/durafmt"
+ "github.com/tkuchiki/parsetime"
+ "time"
+)
+
+type Diff struct {
+ Start string
+ End string
+ Brief bool
+}
+
+type OptionsDiff func(*Diff)
+
+func NewDiff(options ...OptionsDiff) *Diff {
+ diff := &Diff{}
+ for _, opt := range options {
+ opt(diff)
+ }
+ return diff
+}
+
+func DiffWithStart(start string) OptionsDiff {
+ return func(opt *Diff) {
+ opt.Start = start
+ }
+}
+
+func DiffWithEnd(end string) OptionsDiff {
+ return func(opt *Diff) {
+ opt.End = end
+ }
+}
+
+func DiffWithBrief(brief bool) OptionsDiff {
+ return func(opt *Diff) {
+ opt.Brief = brief
+ }
+}
+
+func (diff *Diff) String() string {
+ return fmt.Sprintf("start:%v end:%v brief:%v", diff.Start, diff.End, diff.Brief)
+}
+
+// CalculateDiff return the time difference and also set dt.Diff
+// first try to parse with carbon, fallback to parsing with now if carbon fails to parse
+func (diff *Diff) CalculateDiff() (string, time.Duration, error) {
+ var start, end time.Time
+
+ alpha := carbon.Parse(convertRelativeDateToActual(diff.Start))
+ if alpha.Error != nil {
+ // fmt.Println("alpha:", alpha.Error)
+ p, err := parsetime.NewParseTime()
+ if err != nil {
+ return "", 0, err
+ }
+ start, err = p.Parse(diff.Start)
+ if err != nil {
+ return "", 0, err
+ }
+ } else {
+ start = alpha.StdTime()
+ }
+
+ omega := carbon.Parse(convertRelativeDateToActual(diff.End))
+ if omega.Error != nil {
+ // fmt.Println("omega:", omega.Error)
+ p, err := parsetime.NewParseTime()
+ if err != nil {
+ return "", 0, err
+ }
+ end, err = p.Parse(diff.End)
+ if err != nil {
+ return "", 0, err
+ }
+ } else {
+ end = omega.StdTime()
+ }
+
+ duration := end.Sub(start)
+ parsed := durafmt.Parse(duration)
+ difference := fmt.Sprintf("%v", parsed)
+ if diff.Brief {
+ difference = shrinkPeriod(difference)
+ }
+ return difference, duration, nil
+}
diff --git a/dur.go b/dur.go
new file mode 100644
index 0000000..40607ab
--- /dev/null
+++ b/dur.go
@@ -0,0 +1,362 @@
+package DateTimeMate
+
+import (
+ "fmt"
+ "github.com/golang-module/carbon/v2"
+ "github.com/lestrrat-go/strftime"
+ "github.com/tkuchiki/parsetime"
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// used for Dur.Op
+const (
+ Add = iota
+ Sub
+)
+
+type Dur struct {
+ From string
+ Op int
+ Period string
+ Repeat int
+ Until string
+ OutputFormat string
+}
+
+type OptionsDur func(*Dur)
+
+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; dates in uppercase; times in lowercase"
+)
+
+var carbonFuncs = map[string]interface{}{
+ "year": [2]interface{}{carbon.Carbon.AddYears, carbon.Carbon.SubYears},
+ "month": [2]interface{}{carbon.Carbon.AddMonths, carbon.Carbon.SubMonths},
+ "week": [2]interface{}{carbon.Carbon.AddWeeks, carbon.Carbon.SubWeeks},
+ "day": [2]interface{}{carbon.Carbon.AddDays, carbon.Carbon.SubDays},
+ "hour": [2]interface{}{carbon.Carbon.AddHours, carbon.Carbon.SubHours},
+ "minute": [2]interface{}{carbon.Carbon.AddMinutes, carbon.Carbon.SubMinutes},
+ "second": [2]interface{}{carbon.Carbon.AddSeconds, carbon.Carbon.SubSeconds},
+ "millisecond": [2]interface{}{carbon.Carbon.AddMilliseconds, carbon.Carbon.SubMilliseconds},
+ "microsecond": [2]interface{}{carbon.Carbon.AddMicroseconds, carbon.Carbon.SubMicroseconds},
+ "nanosecond": [2]interface{}{carbon.Carbon.AddNanoseconds, carbon.Carbon.SubNanoseconds},
+}
+
+var expandedRegexp = regexp.MustCompile(expanded)
+
+func NewDur(options ...OptionsDur) *Dur {
+ dur := &Dur{}
+ for _, opt := range options {
+ opt(dur)
+ }
+ return dur
+}
+
+func DurWithFrom(from string) OptionsDur {
+ return func(dur *Dur) {
+ dur.From = from
+ }
+}
+
+func DurWithOp(op int) OptionsDur {
+ return func(dur *Dur) {
+ dur.Op = op
+ }
+}
+
+func DurWithDur(d string) OptionsDur {
+ return func(dur *Dur) {
+ dur.Period = d
+ }
+}
+
+func DurWithRepeat(repeat int) OptionsDur {
+ return func(dur *Dur) {
+ dur.Repeat = repeat
+ }
+}
+func DurWithUntil(until string) OptionsDur {
+ return func(dur *Dur) {
+ dur.Until = until
+ }
+}
+
+func DurWithOutputFormat(outputFormat string) OptionsDur {
+ return func(dur *Dur) {
+ dur.OutputFormat = outputFormat
+ }
+}
+
+func (dur *Dur) String() string {
+ //return fmt.Sprintf("From: %v Period:%v Op:%v Repeat:%v Until:%v OutputFormat:%v", dur.From, dur.Period, dur.Op, dur.Repeat, dur.Until, dur.OutputFormat)
+ return fmt.Sprintf("%v,%v,%v,%v,%v,%v", dur.From, dur.Period, dur.Op, dur.Repeat, dur.Until, dur.OutputFormat)
+}
+
+func (dur *Dur) Add() ([]string, error) {
+ return dur.addOrSub(Add)
+}
+
+func (dur *Dur) Sub() ([]string, error) {
+ return dur.addOrSub(Sub)
+}
+
+func (dur *Dur) addOrSub(op int) ([]string, error) {
+ if dur.Repeat > 0 && dur.Until != "" {
+ return nil, fmt.Errorf("repeat & until are mutually exclusive")
+ }
+
+ var all []string
+ var err error
+ if dur.Repeat == 0 && dur.Until == "" {
+ var c string
+ c, err = calculate(dur.From, dur.Period, op)
+ if err != nil {
+ return nil, err
+ }
+ all = []string{c}
+ } else if dur.Repeat > 0 && dur.Until == "" {
+ from := dur.From
+ for i := 0; i < dur.Repeat; i++ {
+ from, err = calculate(from, dur.Period, op)
+ if err != nil {
+ return nil, err
+ }
+ all = append(all, from)
+ }
+ } else if dur.Repeat == 0 && dur.Until != "" {
+ var f, u time.Time
+ var err error
+
+ until := convertRelativeDateToActual(dur.Until)
+ p, err := parsetime.NewParseTime()
+ if err != nil {
+ return nil, err
+ }
+ u, err = p.Parse(until)
+ if err != nil {
+ return nil, err
+ }
+
+ from := convertRelativeDateToActual(dur.From)
+ for {
+ from, err = calculate(from, dur.Period, op)
+ if err != nil {
+ return nil, err
+ }
+
+ p, err := parsetime.NewParseTime()
+ if err != nil {
+ return nil, err
+ }
+ f, err = p.Parse(from)
+ if err != nil {
+ return nil, err
+ }
+
+ if Add == op { // FIXME
+ if f.After(u) {
+ break
+ }
+ } else {
+ if f.Before(u) {
+ break
+ }
+ }
+ all = append(all, from)
+ }
+ }
+
+ if len(dur.OutputFormat) > 0 && len(all) > 0 {
+ var allWithFormat []string
+ for _, a := range all {
+ formatted, err := dur.setOutputFormat(a)
+ if err != nil {
+ return nil, err
+ }
+ allWithFormat = append(allWithFormat, formatted)
+ }
+ return allWithFormat, nil
+ }
+ return all, nil
+}
+
+//func (dur *Period) Sub() (string, error) {
+// return calculate(dur.From, dur.Period, Sub)
+//}
+
+func (dur *Dur) setOutputFormat(arg string) (string, error) {
+ f, err := strftime.New(dur.OutputFormat)
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ p, err := parsetime.NewParseTime()
+ if err != nil {
+ return "", err
+ }
+ s, err := p.Parse(arg)
+ if err != nil {
+ return "", err
+ }
+ output := f.FormatString(s)
+ return output, nil
+}
+
+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)
+ }
+ }
+
+ from = convertRelativeDateToActual(from)
+ p, err := parsetime.NewParseTime()
+ if err != nil {
+ return "", err
+ }
+ f, err := p.Parse(from)
+ if err != nil {
+ return "", err
+ }
+
+ to := carbon.CreateFromStdTime(f)
+ if to.Error != nil {
+ return "", to.Error
+ }
+ err = validatePeriod(period)
+ if err != nil {
+ return "", err
+ }
+
+ for i := range periodMatches {
+ amount := periodMatches[i][1]
+ num, err := strconv.Atoi(amount)
+ if err != nil {
+ return "", err
+ }
+ word := periodMatches[i][2]
+ // to understand this line of code, read: ChatGPT_Explanation.md
+ to = carbonFuncs[removeTrailingS(word)].([2]interface{})[index].(func(carbon.Carbon, int) carbon.Carbon)(to, num)
+ // fmt.Printf(" to: %v | %v | %v\n", num, word, to)
+ }
+ return to.ToString(), nil
+}
+
+// validatePeriod ensure all words in "period" are a valid time duration
+func validatePeriod(period string) error {
+ wordsOnlyRe := regexp.MustCompile(wordsOnly)
+ matches := wordsOnlyRe.FindAllString(period, -1)
+ for _, word := range matches {
+ // fmt.Println("word:", word)
+ _, ok := carbonFuncs[removeTrailingS(word)]
+ if !ok {
+ return fmt.Errorf("[validatePeriod] Invalid period: %s", word)
+ }
+ }
+ return nil
+}
+
+// removeTrailingS convert plural to singular, such as "hours" to "hour"
+func removeTrailingS(s string) string {
+ if len(s) > 0 && s[len(s)-1] == 's' {
+ return s[:len(s)-1]
+ }
+ return s
+}
+
+// shrinkPeriod convert a period into a brief period
+// only allow one replacement per each period
+// Ex: 1 hour 2 minutes 3 seconds => 1h2m3s
+// FIXME: almost redundant code for plural & singular -- try to fix with strings.NewReplacer
+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, " ", "")
+}
+
+// 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
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..3c54933
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,18 @@
+module github.com/jftuga/DateTimeMate
+
+go 1.22.4
+
+require (
+ github.com/golang-module/carbon/v2 v2.3.12
+ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
+ github.com/lestrrat-go/strftime v1.0.6
+ github.com/spf13/cobra v1.8.1
+ github.com/tkuchiki/parsetime v0.3.0
+)
+
+require (
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/tkuchiki/go-timezone v0.2.3 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..f617856
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,74 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/crackcomm/go-clitable v0.0.0-20151121230230-53bcff2fea36/go.mod h1:XiV36mPegOHv+dlkCSCazuGdQR2BUTgIZ2FKqTTHles=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang-module/carbon/v2 v2.3.12 h1:VC1DwN1kBwJkh5MjXmTFryjs5g4CWyoM8HAHffZPX/k=
+github.com/golang-module/carbon/v2 v2.3.12/go.mod h1:HNsedGzXGuNciZImYP2OMnpiwq/vhIstR/vn45ib5cI=
+github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=
+github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
+github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
+github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
+github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/goveralls v0.0.9/go.mod h1:FRbM1PS8oVsOe9JtdzAAXM+DsvDMMHcM1C7drGJD8HY=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tkuchiki/go-timezone v0.2.2/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
+github.com/tkuchiki/go-timezone v0.2.3 h1:D3TVdIPrFsu9lxGxqNX2wsZwn1MZtTqTW0mdevMozHc=
+github.com/tkuchiki/go-timezone v0.2.3/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
+github.com/tkuchiki/parsetime v0.3.0 h1:cvblFQlPeAPJL8g6MgIGCHnnmHSZvluuY+hexoZCNqc=
+github.com/tkuchiki/parsetime v0.3.0/go.mod h1:OJkQmIrf5Ao7R+WYIdITPOfDVj8LmnHGCfQ8DTs3LCA=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=