Skip to content

Commit 0b47aec

Browse files
authored
feat: add xtime.FormatDuration and xtime.ParseDuration (#13)
1 parent 4893d4c commit 0b47aec

File tree

9 files changed

+1252
-6
lines changed

9 files changed

+1252
-6
lines changed

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ comes in the form of a collection of packages.
1111

1212
## Dependencies
1313

14-
The packages are dependency free meaning. Packages added to this module must not
15-
use any external dependencies unless listed below.
16-
14+
The packages are dependency free, meaning they must not use any external
15+
dependencies unless explicitly listed.
1716
Exceptions:
1817

1918
- `golang.org/x/*` - maintained by go and dependency free
@@ -28,20 +27,19 @@ Do *NOT* add exceptions to this list without peer review.
2827

2928
- Prefix names for packages that mirror a go standard library package with `x`.
3029
- Prefix names for packages that are likely to mirror future go standard library
31-
Packages with `x`.
30+
packages with `x`.
3231
- Use singular names for package (except in the mentioned cases).
3332

3433
## Testing
3534

3635
- Unit testing is mandatory.
37-
- Go for > 95% coverage, preferably 100%.
36+
- Go for > 90% coverage, preferably 100%.
3837

3938
## Documentation
4039

4140
- Document all exported (public) identifiers
4241
- Maintain a `doc.go` in each package with introduction, installation
4342
instructions and usage examples.
44-
- Use `make gen` to generate `README.md` files
4543

4644
### doc.go minimal content
4745

@@ -61,6 +59,7 @@ package mypkg
6159
- `xslices` - slice data type functions
6260
- `xstrings` - string data type functions
6361
- `xstructs` - struct data type functions
62+
- `xtime` - time functions
6463

6564
## Installation
6665

xtime/clean_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package xtime
2+
3+
import (
4+
"testing"
5+
6+
"github.com/neticdk/go-stdlib/assert"
7+
)
8+
9+
func TestClean(t *testing.T) {
10+
testCases := []struct {
11+
input string
12+
expected string
13+
}{
14+
{
15+
input: "1 hour 30 minutes",
16+
expected: "1 hour 30 minutes",
17+
},
18+
{
19+
input: "1h30m",
20+
expected: "1h30m",
21+
},
22+
{
23+
input: "1.5 seconds",
24+
expected: "1.5 seconds",
25+
},
26+
{
27+
input: "1 year, 2 hour, and 5s",
28+
expected: "1 year 2 hour 5s",
29+
},
30+
{
31+
input: ",1 years",
32+
expected: " 1 years",
33+
},
34+
{
35+
input: "and 1 minute",
36+
expected: "1 minute",
37+
},
38+
{
39+
input: ", 4 minutes",
40+
expected: " 4 minutes",
41+
},
42+
{
43+
input: "",
44+
expected: "",
45+
},
46+
}
47+
48+
for _, tc := range testCases {
49+
t.Run(tc.input, func(t *testing.T) {
50+
actual := clean(tc.input)
51+
assert.Equal(t, actual, tc.expected)
52+
})
53+
}
54+
}

xtime/const.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package xtime
2+
3+
const (
4+
floatBitSize = 64
5+
)

xtime/doc.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Package xtime provides functionality for working with time.
2+
//
3+
// # Duration Formatting (FormatDuration)
4+
//
5+
// The FormatDuration function converts a time.Duration into a string composed
6+
// of multiple time units (e.g., "1h 5m 30s", "2 days, 3 hours"). This offers
7+
// more detailed breakdowns than the standard time.Duration.String() method,
8+
// which typically scales to the largest appropriate single unit (e.g.,
9+
// "1m30.5s").
10+
//
11+
// duration := 2*xtime.Day + 3*time.Hour + 15*time.Minute + 30*time.Second +
12+
// 500*time.Millisecond
13+
//
14+
// Default formatting (short style, max unit Day, min unit Second, no
15+
// rounding):
16+
//
17+
// fmt.Println(xtime.FormatDuration(duration)) // Output: 2d 3h 15m 30s
18+
//
19+
// ## Custom formatting examples
20+
//
21+
// Long style, max 2 components, rounding enabled:
22+
//
23+
// fmt.Println(xtime.FormatDuration(duration,
24+
// xtime.WithStyle(xtime.FormatStyleLong),
25+
// xtime.WithMaxComponents(2),
26+
// xtime.WithRounding(),
27+
// )) // Output: 2 days, 3 hours
28+
//
29+
// Compact style:
30+
//
31+
// fmt.Println(xtime.FormatDuration(duration,
32+
// xtime.WithStyle(xtime.FormatStyleCompact),
33+
// )) // Output: 2d3h
34+
//
35+
// Short style, rounding enabled, only seconds and smaller displayed:
36+
//
37+
// fmt.Println(xtime.FormatDuration(time.Second+600*time.Millisecond,
38+
// xtime.WithRounding(),
39+
// xtime.WithMinUnit(time.Millisecond),
40+
// )) // Output: 2s (Original: 1s 600ms, rounded up)
41+
//
42+
// Formatting Options (Functional Options Pattern):
43+
// - WithMaxUnit(unit time.Duration): Sets the largest unit for decomposition
44+
// (default: Day).
45+
// - WithMinUnit(unit time.Duration): Sets the smallest unit to display
46+
// (default: Second). Remainder is truncated or rounded.
47+
// - WithRounding(): Enables rounding of the MinUnit based on the remainder.
48+
// The duration is adjusted by adding half of MinUnit before decomposition.
49+
// - WithoutRounding(): Disables rounding (default). Remainder is truncated.
50+
// - WithMaxComponents(n int): Limits output to at most 'n' components
51+
// (default: 0 = unlimited).
52+
// - WithStyle(style FormatStyle): Sets output style (short, long, long-and).
53+
// - WithSeparator(sep string): Custom separator between components (default
54+
// depends on style).
55+
// - WithConjunction(conj string): Custom conjunction (" and " by default)
56+
// used before the last component in "long-and" style.
57+
//
58+
// # Duration Parsing (ParseDuration)
59+
//
60+
// The ParseDuration function converts a human-readable string representation
61+
// into a time.Duration value. It accepts various formats, including combined
62+
// units and common abbreviations.
63+
//
64+
// d1, err := xtime.ParseDuration("1h 30m 15s")
65+
// d2, err := xtime.ParseDuration("1.5hours 10sec") // Combined number/unit and spaces work
66+
// d3, err := xtime.ParseDuration("10 years, 2 months, 5 days") // Approximate units allowed
67+
// d4, err := xtime.ParseDuration("3d12h")
68+
//
69+
// Input String Processing:
70+
// 1. Cleaning: Leading/trailing whitespace is trimmed, "and " sequences are
71+
// removed, and commas are replaced with spaces.
72+
// 2. Tokenization: The cleaned string is split by spaces. Tokens containing
73+
// both numbers and letters (e.g., "10years", "1h30m", "h1") are further
74+
// split into number and unit parts (e.g., "10", "years", "1", "h", "30",
75+
// "m", "h", "1").
76+
// 3. Parsing:
77+
// - If only one token results and it's a valid number, it's interpreted as
78+
// seconds.
79+
// - Otherwise, tokens are processed in pairs (value, unit). The value must
80+
// be a number (integer or float), and the unit must be one of the
81+
// recognized unit strings (e.g., "h", "hour", "hours", "d", "day", "days",
82+
// "mo", "month", "y", "year").
83+
// - Parsing fails if the token sequence is invalid (e.g., odd number of
84+
// tokens, non-number where value is expected, unknown unit).
85+
//
86+
// Error Handling:
87+
// - Returns a specific error type (*DurationParseError) containing details
88+
// about the failure, including the original input, problematic token, and index.
89+
//
90+
// # Units and Approximations
91+
//
92+
// The package defines standard fixed-duration units (Week, Day) and also
93+
// provides approximate average durations for Month (MonthApprox) and Year
94+
// (YearApprox) based on the Gregorian calendar average (365.2425 days/year).
95+
//
96+
// The YearsFromDuration(d time.Duration) function converts a duration to an
97+
// approximate number of years using YearApprox. Useful for rough estimations
98+
// only.
99+
//
100+
// Note on Units Discrepancy: ParseDuration can parse approximate units like
101+
// "month" (mo) and "year" (y) based on the average durations (MonthApprox,
102+
// YearApprox). However, FormatDuration does not format durations using these
103+
// approximate units; it will decompose them into weeks, days, etc., for more
104+
// precise representation based on the fixed time.Duration value.
105+
//
106+
// Note: For calendar-accurate calculations involving months and years (which
107+
// vary in length), always operate on time.Time values using functions like
108+
// time.AddDate and time.Sub, rather than relying solely on time.Duration
109+
// arithmetic.
110+
package xtime

0 commit comments

Comments
 (0)