-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Enhance ParseDuration with configurable parsing options
Introduce ParseDuration() as the primary and simplest entrypoint for parsing durations. By default it operates in "single-unit mode", meaning only one unit type is allowed per input string (e.g., "1d" as opposed to permitting "1d3h5m"). Assumes milliseconds as the default unit when none is specified.
- Loading branch information
Showing
6 changed files
with
1,163 additions
and
367 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,62 +1,74 @@ | ||
# parse time durations, with support for days | ||
# Parse time durations, with support for days | ||
|
||
This is a Go library that parses duration strings in a format similar | ||
to time.ParseDuration, with the additional capability of handling | ||
duration strings specifying a number of days ("d"). This functionality | ||
is not available in the built-in time.ParseDuration function. It also | ||
differs by not accepting negative values. This package was primarily | ||
created for validating HAProxy timeout values. | ||
A Go library to parse time duration strings similar to | ||
time.ParseDuration, but with extended functionality. In addition to | ||
the standard duration units like "h", "m", and "s", it also supports | ||
days (represented as "d"), which are unavailable in the built-in | ||
time.ParseDuration function. All durations parsed by this package are | ||
ensured to be non-negative and won't overflow the time.Duration value | ||
type. Furthermore, the parsed value cannot exceed HAProxy's maximum | ||
timeout value, ensuring compatibility with HAProxy's configuration | ||
constraints. | ||
|
||
The CLI utility `haproxy-timeout-checker` is an example of using the | ||
package. It validates the time duration using `ParseDuration` and also | ||
checks to see if the duration exceeds HAProxy's maximum. | ||
The CLI utility `haproxytimeout` is an example of using the package. | ||
The utility parses a duration string, printing the parsed value as a | ||
pair; "human-readable duration" -> "millisecond duration". | ||
|
||
```console | ||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "9223372036s" | ||
duration 9223372036000ms exceeds HAProxy's maximum value of 2147483647ms | ||
$ go build ./cmd/haproxytimeout | ||
|
||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "2147483647ms" | ||
2147483647 | ||
usage: haproxytimeout [-m] [-j] <duration> | ||
-m: Print the maximum duration HAProxy can tolerate and exit. | ||
-j: Print results in JSON format. (e.g., haproxytimeout -j <duration> | jq .milliseconds | ||
|
||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "2147483648ms" | ||
duration 2147483648ms exceeds HAProxy's maximum value of 2147483647ms | ||
Input Assumptions: | ||
If the input is a plain number (or a decimal without a unit), it's | ||
assumed to be in milliseconds. For example: 'haproxytimeout 1500' | ||
is assumed to be '1500ms'. | ||
|
||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "1d" | ||
86400000 | ||
Output Format: | ||
The output maps the human-readable format to the milliseconds | ||
format. For example: 'haproxytimeout 2h30m' will output '2h30m -> | ||
9000000ms'. The right-hand side is always in milliseconds, making it | ||
suitable for direct use in a haproxy.cfg file. | ||
|
||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "1d 1s" | ||
86401000 | ||
Available units: | ||
d days | ||
h: hours | ||
m: minutes | ||
s: seconds | ||
ms: milliseconds | ||
us: microseconds | ||
|
||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "1d 3h 10m 20s 100ms 9999us" | ||
97820109 | ||
|
||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go 5000 | ||
5000 | ||
Example usage: | ||
haproxytimeout 2h30m -> Convert 2 hours 30 minutes to milliseconds. | ||
haproxytimeout 1500ms -> Convert 1500 milliseconds to a human-readable format. | ||
haproxytimeout -j 1h17m -> Get JSON output for 1h 17minutes | ||
haproxytimeout -m -> Show the maximum duration HAProxy can tolerate. | ||
``` | ||
|
||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "5000 999999ms" | ||
5000 999999ms | ||
^ | ||
error: invalid unit order | ||
## Examples | ||
|
||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "1d 1f" | ||
1d 1f | ||
^ | ||
error: invalid unit | ||
```sh | ||
$ ./haproxytimeout 30m | ||
30m -> 1800000ms | ||
``` | ||
|
||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "1d 1d" | ||
1d 1d | ||
^ | ||
error: invalid unit order | ||
```sh | ||
$ ./haproxytimeout 1d12h | ||
1d12h -> 129600000ms | ||
``` | ||
|
||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "1d 5m 1230ms" | ||
86701230 | ||
```sh | ||
# Print maximum value allowed by HAProxy. | ||
$ ./haproxytimeout -m | ||
24d20h31m23s647ms -> 2147483647ms | ||
``` | ||
|
||
# Note: Spaces are optional. | ||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "1d5m" | ||
86700000 | ||
```sh | ||
% ./haproxytimeout -j 17m | ||
{"human_readable":"17m","milliseconds":1020000} | ||
|
||
$ go run cmd/haproxy-timeout-checker/haproxy-timeout-checker.go "9223372037s" | ||
9223372037s | ||
^ | ||
error: underflow | ||
% ./haproxytimeout -j 17m | jq .milliseconds | ||
1020000 | ||
``` |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"flag" | ||
"fmt" | ||
"os" | ||
"time" | ||
|
||
"github.com/frobware/haproxytime" | ||
) | ||
|
||
func printErrorWithPosition(input string, err error, position int) { | ||
fmt.Fprintln(os.Stderr, err) | ||
fmt.Fprintln(os.Stderr, input) | ||
fmt.Fprintf(os.Stderr, "%"+fmt.Sprint(position)+"s", "") | ||
fmt.Fprintln(os.Stderr, "^") | ||
} | ||
|
||
func formatDuration(d time.Duration) string { | ||
const ( | ||
Day = time.Hour * 24 | ||
Month = Day * 30 | ||
Year = Day * 365 | ||
Microsecond = time.Microsecond | ||
) | ||
|
||
years := d / Year | ||
d -= years * Year | ||
|
||
months := d / Month | ||
d -= months * Month | ||
|
||
days := d / Day | ||
d -= days * Day | ||
|
||
hours := d / time.Hour | ||
d -= hours * time.Hour | ||
|
||
minutes := d / time.Minute | ||
d -= minutes * time.Minute | ||
|
||
seconds := d / time.Second | ||
d -= seconds * time.Second | ||
|
||
milliseconds := d / time.Millisecond | ||
d -= milliseconds * time.Millisecond | ||
|
||
microseconds := d / Microsecond | ||
|
||
var result string | ||
if years > 0 { | ||
result += fmt.Sprintf("%dy", years) | ||
} | ||
if months > 0 { | ||
result += fmt.Sprintf("%dmo", months) | ||
} | ||
if days > 0 { | ||
result += fmt.Sprintf("%dd", days) | ||
} | ||
if hours > 0 { | ||
result += fmt.Sprintf("%dh", hours) | ||
} | ||
if minutes > 0 { | ||
result += fmt.Sprintf("%dm", minutes) | ||
} | ||
if seconds > 0 { | ||
result += fmt.Sprintf("%ds", seconds) | ||
} | ||
if milliseconds > 0 { | ||
result += fmt.Sprintf("%dms", milliseconds) | ||
} | ||
if microseconds > 0 { | ||
result += fmt.Sprintf("%dus", microseconds) | ||
} | ||
|
||
return result | ||
} | ||
|
||
func main() { | ||
max := flag.Bool("m", false, "Print the maximum duration HAProxy can tolerate and exit.") | ||
jsonOutput := flag.Bool("j", false, "Print results in JSON format.") | ||
flag.Parse() | ||
|
||
args := flag.Args() | ||
|
||
if len(args) == 0 && !*max { | ||
fmt.Println(`usage: haproxytimeout [-m] [-j] <duration> | ||
-m: Print the maximum duration HAProxy can tolerate and exit. | ||
-j: Print results in JSON format. (e.g., haproxytimeout -j <duration> | jq .milliseconds | ||
Input Assumptions: | ||
If the input is a plain number (or a decimal without a unit), it's | ||
assumed to be in milliseconds. For example: 'haproxytimeout 1500' | ||
is assumed to be '1500ms'. | ||
Output Format: | ||
The output maps the human-readable format to the milliseconds | ||
format. For example: 'haproxytimeout 2h30m' will output '2h30m -> | ||
9000000ms'. The right-hand side is always in milliseconds, making it | ||
suitable for direct use in a haproxy.cfg file. | ||
Available units: | ||
d days | ||
h: hours | ||
m: minutes | ||
s: seconds | ||
ms: milliseconds | ||
us: microseconds | ||
Example usage: | ||
haproxytimeout 2h30m -> Convert 2 hours 30 minutes to milliseconds. | ||
haproxytimeout 1500ms -> Convert 1500 milliseconds to a human-readable format. | ||
haproxytimeout -j 1h17m -> Get JSON output for 1h 17minutes | ||
haproxytimeout -m -> Show the maximum duration HAProxy can tolerate.`) | ||
os.Exit(1) | ||
} | ||
|
||
type outputResult struct { | ||
HumanReadable string `json:"human_readable"` | ||
Milliseconds int64 `json:"milliseconds"` | ||
} | ||
|
||
// Output function based on jsonOutput flag. | ||
output := func(human string, millis int64) { | ||
if human == "" { | ||
human = formatDuration(time.Duration(millis) * time.Millisecond) | ||
} | ||
|
||
if *jsonOutput { | ||
res := outputResult{ | ||
HumanReadable: human, | ||
Milliseconds: millis, | ||
} | ||
jsonRes, err := json.Marshal(res) | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err) | ||
os.Exit(6) | ||
} | ||
fmt.Println(string(jsonRes)) | ||
} else { | ||
fmt.Printf("%s -> %dms\n", human, millis) | ||
} | ||
} | ||
|
||
// Handle max duration request. | ||
if *max { | ||
maxDuration := haproxytime.MaxTimeout.Milliseconds() | ||
// Always format max duration as human-readable. | ||
output(formatDuration(haproxytime.MaxTimeout), maxDuration) | ||
return | ||
} | ||
|
||
parserCfg, err := haproxytime.NewParserConfig(haproxytime.UnitMillisecond, haproxytime.ModeMultiUnit) | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "error: failed to create parser configuration: %v\n", err) | ||
os.Exit(2) | ||
} | ||
|
||
duration, err := haproxytime.ParseDurationWithConfig(args[0], parserCfg) | ||
if err != nil { | ||
var se *haproxytime.SyntaxError | ||
var oe *haproxytime.OverflowError | ||
switch { | ||
case errors.As(err, &se): | ||
printErrorWithPosition(args[0], err, se.Position) | ||
os.Exit(3) | ||
case errors.As(err, &oe): | ||
printErrorWithPosition(args[0], err, oe.Position) | ||
os.Exit(4) | ||
default: | ||
panic(err) | ||
} | ||
} | ||
|
||
if haproxytime.DurationExceedsMaxTimeout(duration) { | ||
maxHumanFormat := formatDuration(haproxytime.MaxTimeout) | ||
humanFormat := formatDuration(time.Duration(duration.Milliseconds()) * time.Millisecond) | ||
fmt.Fprintf(os.Stderr, "%s exceeds HAProxy's maximum duration %s (%vms)\n", | ||
humanFormat, maxHumanFormat, haproxytime.MaxTimeout.Milliseconds()) | ||
os.Exit(5) | ||
} | ||
|
||
output(formatDuration(duration), duration.Milliseconds()) | ||
} |
Oops, something went wrong.