Skip to content

Commit c614523

Browse files
committed
Initial checkin
1 parent 9dc5bd9 commit c614523

60 files changed

Lines changed: 3258 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ChangeLog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
### 1.0.0 -- 2025-07-16
2+
* Initial public release.

Makefile

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
ROOT=/usr/local
2+
# ROOT=$(HOME)
3+
BINDIST=$(ROOT)/bin
4+
MANDIST=$(ROOT)/man/man1
5+
CMD=fad
6+
MANPAGE=fad.1
7+
8+
GENERATED=version.go
9+
CMDDEPENDS=$(GENERATED) *.go Makefile $(MANPAGE)
10+
11+
all: $(CMD)
12+
13+
$(CMD): $(CMDDEPENDS)
14+
go build
15+
16+
version.go: generate_version.sh ChangeLog.md go.mod Makefile
17+
sh generate_version.sh ChangeLog.md version.go
18+
19+
.PHONY: fmt
20+
fmt:
21+
gofmt -s -w .
22+
23+
.PHONY: clean
24+
clean:
25+
go clean
26+
rm -f fad.exe
27+
28+
.PHONY: vet
29+
vet: $(GENERATED)
30+
go vet ./...
31+
mandoc -Tlint $(MANPAGE); exit 0
32+
33+
.PHONY: install
34+
install: $(BINDIST)/$(CMD) $(MANDIST)/$(MANPAGE)
35+
36+
.PHONY: test tests
37+
38+
test tests: $(GENERATED)
39+
go test -race -v
40+
41+
$(BINDIST)/$(CMD): $(CMD) Makefile
42+
install -d -m u=rwx,go=rx $(BINDIST) # Ensure destination exists
43+
install -p -m a=rx $(CMD) $(BINDIST)
44+
@echo $(CMD) installed in $(BINDIST)
45+
@echo
46+
47+
$(MANDIST)/$(MANPAGE): $(MANPAGE) Makefile
48+
install -d -m u=rwx,go=rx $(MANDIST) # Ensure destination exists
49+
install -p -m a=r $(MANPAGE) $(MANDIST)
50+
@echo $(MANPAGE) installed in $(MANDIST)
51+
@echo
52+
53+
.PHONY: windows
54+
windows: fad.exe
55+
fad.exe: $(CMDDEPENDS)
56+
@echo 'Building for Windows amd64 (10 or higher)'
57+
@GOOS=windows GOARCH=amd64 go build
58+
@file $(CMD).exe

age.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"time"
7+
)
8+
9+
const (
10+
second int64 = 1
11+
minute = 60 * second
12+
hour = 60 * minute
13+
day = 24 * hour
14+
week = 7 * day
15+
year = (365 * day) + (5 * hour) + (49 * minute) + (12 * second) // Gregorian
16+
month = year / 12
17+
ageMaxSeconds = 999 * year // Arbitrary, but pretty value
18+
)
19+
20+
var (
21+
ageUnitToSeconds = map[string]int64{
22+
"s": second,
23+
"m": minute,
24+
"h": hour,
25+
"D": day,
26+
"W": week,
27+
"M": month,
28+
"Y": year,
29+
}
30+
31+
ageSecondsToUnit = []struct {
32+
upper int64
33+
unit string
34+
}{
35+
{year, "Y"}, // Year
36+
{month, "M"}, // Month
37+
{week, "W"}, // Week
38+
{day, "D"}, // Day
39+
{hour, "h"}, // hour
40+
{minute, "m"}, // minute
41+
}
42+
)
43+
44+
// age implements flag.Value as an alternative to flag.Duration because the latter is too
45+
// fine-grained. age accepts values that are more typical of what people care about for
46+
// files such as days, hours and weeks since modified. The calculation for seconds is:
47+
// time.Now().Sub(candidateTime).Seconds() which will normally be positive for anything in
48+
// the file system since it will normally have a creation time earlier than "now".
49+
//
50+
// -ve <-----0-----> +ve
51+
// Young Old
52+
type age struct {
53+
seconds int64 // Seconds before "now" - normally positive for file-system objects
54+
multiplier int64 // Based on unit or defaults to 1 if zero
55+
value string // Original Set value as a string - only used during flags processing
56+
}
57+
58+
func (a *age) String() string {
59+
return a.value
60+
}
61+
62+
// setFromTime stores the distance from 'now' to the value 'v'.
63+
func (a *age) setFromTime(now, v time.Time) {
64+
a.seconds = int64(now.Sub(v).Seconds())
65+
}
66+
67+
// gt returns true if 'a' is greater than 'u' - that is, older. Equal ages are not greater
68+
// than.
69+
//
70+
// If useMultiplier is true, truncate the ages to the multiplier value. The idea is that a
71+
// 3W value equals a 3.2W value and is thus not greater than. The goal of useMultiplier is
72+
// to ensure that a comparison of ages which produce identical compactString() values
73+
// result in a not greater than outcome.
74+
func (a *age) gt(u age, truncateToMultiplier bool) bool {
75+
var multiplier int64
76+
multiplier = second // Default multiplier
77+
if truncateToMultiplier {
78+
if u.multiplier > 0 {
79+
multiplier = u.multiplier
80+
}
81+
if a.multiplier > 0 {
82+
multiplier = a.multiplier
83+
}
84+
}
85+
aSecs := (a.seconds + multiplier/2) / multiplier
86+
uSecs := (u.seconds + multiplier/2) / multiplier
87+
88+
return aSecs > uSecs
89+
}
90+
91+
// le returns true if 'a' is less than or equal to 'u'. Use this when you want equal age
92+
// comparisons to favour the receiver age.
93+
func (a *age) le(u age) bool {
94+
return a.seconds <= u.seconds
95+
}
96+
97+
// lt returns true if 'a' is less than 'u' - that is, younger. Equal ages are not younger.
98+
func (a *age) lt(u age) bool {
99+
return a.seconds < u.seconds
100+
}
101+
102+
// Set helps meets the flag.Value interface. It parses and sets the age or rejects with an
103+
// error. Parsing only accepts 1-5 decimal digits followed by a single unit character. No
104+
// leading '0' is allowed to thwart old-timers who might try to sneak in an octal value
105+
// reminiscent of their mainframe era.
106+
//
107+
// The assumption is that age is being set relative to now and thus is stored as a
108+
// positive integer.
109+
func (a *age) Set(s string) (err error) {
110+
if len(s) > 6 || len(s) < 2 {
111+
return fmt.Errorf("Age '%s' must be 1-5 digits + unit", s)
112+
}
113+
l := len(s)
114+
if s[0] == '0' && l > 2 {
115+
return fmt.Errorf("First digit of '%s' cannot be zero (we no grok octal)", s)
116+
}
117+
118+
unit := string(s[l-1])
119+
l-- // Ensure scanner stops prior to unit
120+
121+
// Check multiplier
122+
var ok bool
123+
if a.multiplier, ok = ageUnitToSeconds[unit]; !ok {
124+
return fmt.Errorf("Age '%s' has invalid unit '%s' - expect s,m,h,D,W,M or Y",
125+
s, unit)
126+
}
127+
128+
// Convert decimal string to binary. We could use strconv here...
129+
val, err := strconv.ParseInt(s[:l], 10, 64)
130+
if err != nil {
131+
return strconvTrimError(err)
132+
}
133+
134+
/*
135+
var val int64
136+
var ix int
137+
for ; ix < l; ix++ {
138+
if s[ix] < '0' || s[ix] > '9' {
139+
return fmt.Errorf("Age '%s' has invalid digit '%s'", s, string(s[ix]))
140+
}
141+
val *= 10
142+
val += int64(s[ix] - '0')
143+
}
144+
*/
145+
146+
val *= a.multiplier // Scale out
147+
if val > ageMaxSeconds {
148+
return fmt.Errorf("Age '%s' exceeds maximum value of %d years",
149+
s, ageMaxSeconds/year)
150+
}
151+
152+
a.seconds = val
153+
a.value = s
154+
155+
return
156+
}
157+
158+
// compactString return a compact string which tries to fit in 3 characters by adjusting
159+
// the granularity as the age increases. If the age is in the future, return "fut".
160+
//
161+
// >= 365d -> nnY (Years)
162+
// >= 365/12d -> nnM (Months)
163+
// >= 7d -> nnW (Weeks)
164+
// >= 1d -> nnD (Days)
165+
// >= 1h -> nnh (Hours)
166+
// >= 1m -> nnm (Minutes)
167+
// < 60s -> nns (Seconds)
168+
func (a *age) compactString() string {
169+
if a.seconds < 0 {
170+
return "fut"
171+
}
172+
for _, s := range ageSecondsToUnit {
173+
if a.seconds >= s.upper {
174+
return fmt.Sprintf("%d%s", (a.seconds+s.upper/2)/s.upper, s.unit)
175+
}
176+
}
177+
178+
return fmt.Sprintf("%ds", a.seconds)
179+
}

age_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package main
2+
3+
import (
4+
"strings"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestAgeSet(t *testing.T) {
10+
testCases := []struct {
11+
in string
12+
err string // Contained in error text
13+
out int64 // Only checked if no error (natch)
14+
}{
15+
{"", "1-5 digits", 0}, // # 0
16+
{"123456s", "1-5 digits", 0},
17+
{"01s", "octal", 0},
18+
{"1S", "invalid unit", 0},
19+
{"1t", "invalid unit", 0},
20+
{"1xs", "invalid syntax", 0},
21+
{"1234xs", "invalid syntax", 0},
22+
{"x1234s", "invalid syntax", 0},
23+
{"3000Y", "exceeds", 0},
24+
{"16000M", "exceeds", 0}, // # 9
25+
26+
{"0s", "", 0 * second}, // # 10
27+
{"1s", "", 1 * second},
28+
{"59s", "", 59 * second},
29+
{"60s", "", 1 * minute},
30+
{"61s", "", 61 * second},
31+
{"1m", "", 1 * minute},
32+
{"59m", "", 59 * minute},
33+
{"1h", "", 1 * hour},
34+
{"23h", "", 23 * hour},
35+
{"1D", "", 1 * day},
36+
{"7D", "", 1 * week},
37+
{"1W", "", 1 * week},
38+
{"1Y", "", 1 * year},
39+
}
40+
41+
for ix, tc := range testCases {
42+
var a age
43+
err := a.Set(tc.in)
44+
if err != nil {
45+
if len(tc.err) == 0 {
46+
t.Error(ix, "Unexpected error", err)
47+
continue
48+
}
49+
if !strings.Contains(err.Error(), tc.err) {
50+
t.Errorf("%d Wrong error returned. Want '%s' got '%s'\n", ix, tc.err, err.Error())
51+
continue
52+
}
53+
continue
54+
}
55+
if len(tc.err) > 0 {
56+
t.Error(ix, "Expected error", tc.err)
57+
continue
58+
}
59+
60+
if tc.out != a.seconds {
61+
t.Error(ix, "Wrong value parsed. Expect", tc.out, "got", a.seconds)
62+
continue
63+
}
64+
65+
if a.String() != tc.in {
66+
t.Error(ix, "Set value of", a.String(), "differs from input", tc.in)
67+
continue
68+
}
69+
}
70+
}
71+
72+
func TestAgeGreaterThan(t *testing.T) {
73+
var baby, twin, granny age
74+
baby.seconds = -3
75+
twin.seconds = -3 // Equal ages are not greater than each other
76+
granny.seconds = 1
77+
if !granny.gt(baby, false) {
78+
t.Error("Granny should be GT baby")
79+
}
80+
if baby.gt(twin, false) {
81+
t.Error("Baby should not be greater than twin")
82+
}
83+
if twin.gt(baby, false) {
84+
t.Error("Twin should not be greater than baby")
85+
}
86+
87+
baby.seconds = -3*day + 11*hour
88+
twin.seconds = -3 * day
89+
baby.multiplier = day
90+
twin.multiplier = day
91+
92+
if baby.gt(twin, true) {
93+
t.Error("Truncated baby should not be greater than twin")
94+
}
95+
if twin.gt(baby, true) {
96+
t.Error("Truncated twin should not be greater than baby")
97+
}
98+
}
99+
100+
func TestAgeLessThan(t *testing.T) {
101+
var baby, twin, granny age
102+
baby.seconds = -3
103+
twin.seconds = -3 // Equal ages are not younger than each other
104+
granny.seconds = 1
105+
if !baby.lt(granny) {
106+
t.Error("Baby should be younger than granny")
107+
}
108+
if baby.lt(twin) {
109+
t.Error("Baby should not be younger than twin")
110+
}
111+
if twin.lt(baby) {
112+
t.Error("Twin should not be younger than baby")
113+
}
114+
}
115+
116+
func TestAgeSetFromTime(t *testing.T) {
117+
var baby, granny age
118+
119+
base := time.Date(1000, 1, 1, 0, 0, 0, 0, time.UTC)
120+
year2000 := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
121+
year3000 := time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)
122+
123+
granny.setFromTime(base, year2000)
124+
baby.setFromTime(base, year3000)
125+
126+
if granny.lt(baby) {
127+
t.Error("Granny", granny, "should not be younger than baby", baby)
128+
}
129+
}
130+
131+
func TestAgeCompactString(t *testing.T) {
132+
testCases := []struct {
133+
seconds int64
134+
expect string
135+
}{
136+
{86401 * 366, "1Y"},
137+
{86401 * 99, "3M"},
138+
{86401 * 7 * 2, "2W"},
139+
{86401, "1D"},
140+
{60*60 + 1, "1h"},
141+
{121, "2m"},
142+
{61, "1m"},
143+
{59, "59s"},
144+
{1, "1s"},
145+
{0, "0s"},
146+
{-5, "fut"},
147+
}
148+
149+
var a age
150+
for ix, tc := range testCases {
151+
a.seconds = tc.seconds
152+
s := a.compactString()
153+
if s != tc.expect {
154+
t.Error(ix, "Expect", tc.expect, "got", s)
155+
}
156+
}
157+
}

0 commit comments

Comments
 (0)