Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ go.work.sum
# Editor/IDE
# .idea/
# .vscode/

# Test build artifacts
sleepycli
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/TyostoKarry/sleepycli

go 1.26.1

require github.com/spf13/pflag v1.0.10
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
100 changes: 84 additions & 16 deletions internal/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,110 @@ package validate

import (
"fmt"
"strings"
"time"
)

type Config struct {
WakeTime string
SleepTime string
FromTime string
ToTime string
Buffer int
MinCycles int
MaxCycles int
}

func (c *Config) Validate() error {
if c.WakeTime != "" && c.SleepTime != "" {
return fmt.Errorf("cannot specify both wake time and sleep time")
if err := validateModes(c.WakeTime, c.SleepTime, c.FromTime, c.ToTime); err != nil {
return err
}
if c.WakeTime == "" && c.SleepTime == "" {
return fmt.Errorf("must specify either wake time or sleep time")
if c.FromTime != "" || c.ToTime != "" {
if err := validateWindow(c.FromTime, c.ToTime); err != nil {
return err
}
} else {
if err := validateTimeFlags(c.WakeTime, c.SleepTime); err != nil {
return err
}
}
if err := validateBuffer(c.Buffer); err != nil {
return err
}
return validateCycles(c.MinCycles, c.MaxCycles)
}

func validateModes(wake, sleep, from, to string) error {
windowSet := from != "" || to != ""
wakeSleepSet := wake != "" || sleep != ""
if windowSet && wakeSleepSet {
return fmt.Errorf("cannot use --from/--to with --wake or --sleep")
}
if c.WakeTime != "" {
if _, err := time.Parse("15:04", c.WakeTime); err != nil {
return fmt.Errorf("invalid wake time format: %v", err)
return nil
}

func validateWindow(from, to string) error {
if from == "" {
return fmt.Errorf("--from is required when using --to")
}
if to == "" {
return fmt.Errorf("--to is required when using --from")
}
if err := validateTimeFormat(from); err != nil {
return fmt.Errorf("invalid --from value: %w", err)
}
if err := validateTimeFormat(to); err != nil {
return fmt.Errorf("invalid --to value: %w", err)
}
return nil
}

func validateTimeFlags(wake, sleep string) error {
if wake != "" && sleep != "" {
return fmt.Errorf("cannot specify both --wake and --sleep")
}
if wake == "" && sleep == "" {
return fmt.Errorf("must specify either --wake or --sleep")
}
if wake != "" {
if err := validateTimeFormat(wake); err != nil {
return fmt.Errorf("invalid --wake value: %w", err)
}
}
if c.SleepTime != "" {
if _, err := time.Parse("15:04", c.SleepTime); err != nil {
return fmt.Errorf("invalid sleep time format: %v", err)
if sleep != "" {
if err := validateTimeFormat(sleep); err != nil {
return fmt.Errorf("invalid --sleep value: %w", err)
}
}
if c.Buffer < 0 {
return fmt.Errorf("buffer time cannot be negative")
return nil
}

func validateTimeFormat(s string) error {
_, err := time.Parse("15:04", NormalizeHour(s))
return err
}

func validateBuffer(buffer int) error {
if buffer < 0 {
return fmt.Errorf("--buffer cannot be negative")
}
if c.MinCycles < 0 || c.MaxCycles < 0 {
return fmt.Errorf("cycle counts cannot be negative")
return nil
}

func validateCycles(min, max int) error {
if min < 0 || max < 0 {
return fmt.Errorf("--cycles-min and --cycles-max cannot be negative")
}
if c.MinCycles > c.MaxCycles {
return fmt.Errorf("min cycles cannot be greater than max cycles")
if min > max {
return fmt.Errorf("--cycles-min cannot be greater than --cycles-max")
}
return nil
}

func NormalizeHour(s string) string {
parts := strings.SplitN(s, ":", 2)
if len(parts) == 2 && len(parts[0]) == 1 {
return "0" + s
}
return s
}
64 changes: 57 additions & 7 deletions internal/validate/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ func TestValidateWakeTime(t *testing.T) {
wantErr bool
}{
{"valid wake time", Config{WakeTime: "07:00"}, false},
{"valid wake time format", Config{WakeTime: "7:00"}, false},
{"valid wake time short hour", Config{WakeTime: "7:00"}, false},
{"invalid wake time value", Config{WakeTime: "25:00"}, true},
{"invalid wake time format", Config{WakeTime: "7am"}, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
Expand All @@ -30,6 +31,7 @@ func TestValidateSleepTime(t *testing.T) {
wantErr bool
}{
{"valid sleep time", Config{SleepTime: "22:00"}, false},
{"valid sleep time short hour", Config{SleepTime: "9:00"}, false},
{"invalid sleep time format", Config{SleepTime: "10:00 PM"}, true},
{"invalid sleep time value", Config{SleepTime: "24:00"}, true},
}
Expand All @@ -38,7 +40,27 @@ func TestValidateSleepTime(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestValidateTimeFlagsMutualExclusion(t *testing.T) {
tests := []struct {
name string
config Config
wantErr bool
}{
{"both wake and sleep set", Config{WakeTime: "07:00", SleepTime: "22:00"}, true},
{"neither wake nor sleep set", Config{}, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
Expand All @@ -51,14 +73,15 @@ func TestValidateBuffer(t *testing.T) {
wantErr bool
}{
{"valid buffer", Config{WakeTime: "07:00", Buffer: 15}, false},
{"zero buffer", Config{WakeTime: "07:00", Buffer: 0}, false},
{"negative buffer", Config{WakeTime: "07:00", Buffer: -5}, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
Expand All @@ -78,7 +101,7 @@ func TestValidateMinCycles(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
Expand All @@ -98,7 +121,7 @@ func TestValidateMaxCycles(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
Expand All @@ -111,14 +134,41 @@ func TestValidateMinMaxCycles(t *testing.T) {
wantErr bool
}{
{"valid min and max cycles", Config{WakeTime: "07:00", MinCycles: 4, MaxCycles: 6}, false},
{"equal min and max cycles", Config{WakeTime: "07:00", MinCycles: 6, MaxCycles: 6}, false},
{"min cycles greater than max cycles", Config{WakeTime: "07:00", MinCycles: 7, MaxCycles: 6}, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestValidateWindow(t *testing.T) {
tests := []struct {
name string
config Config
wantErr bool
}{
{"valid window", Config{FromTime: "22:00", ToTime: "07:00"}, false},
{"valid window short hour", Config{FromTime: "9:00", ToTime: "07:00"}, false},
{"missing --to", Config{FromTime: "22:00"}, true},
{"missing --from", Config{ToTime: "07:00"}, true},
{"invalid --from format", Config{FromTime: "10:00 PM", ToTime: "07:00"}, true},
{"invalid --to format", Config{FromTime: "22:00", ToTime: "7am"}, true},
{"window mixed with --wake", Config{FromTime: "22:00", ToTime: "07:00", WakeTime: "07:00"}, true},
{"window mixed with --sleep", Config{FromTime: "22:00", ToTime: "07:00", SleepTime: "22:00"}, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
Expand Down
43 changes: 42 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
package main

func main() {}
import (
"fmt"
"os"

"github.com/spf13/pflag"
)

const version = "0.1.0"

func main() {
var (
wakeFlag string
sleepFlag string
fromFlag string
toFlag string
bufferFlag int
cyclesMaxFlag int
cyclesMinFlag int
versionFlag bool
)

pflag.StringVarP(&wakeFlag, "wake", "w", "", "Calculate bedtimes from wake time (HH:MM)")
pflag.StringVarP(&sleepFlag, "sleep", "s", "", "Calculate wake times from sleep time (HH:MM)")
pflag.StringVarP(&fromFlag, "from", "f", "", "Window sleep time (HH:MM), use with --to")
pflag.StringVarP(&toFlag, "to", "t", "", "Window wake time (HH:MM), use with --from")
pflag.IntVarP(&bufferFlag, "buffer", "b", 15, "Fall asleep buffer in minutes")
pflag.IntVarP(&cyclesMinFlag, "cycles-min", "n", 4, "Minimum cycles to show")
pflag.IntVarP(&cyclesMaxFlag, "cycles-max", "x", 6, "Maximum cycles to show")
pflag.BoolVarP(&versionFlag, "version", "v", false, "Print version")

pflag.Parse()

if versionFlag {
fmt.Println("sleepycli v" + version)
os.Exit(0)
}

if err := validateAndSelectMode(wakeFlag, sleepFlag, fromFlag, toFlag, bufferFlag, cyclesMinFlag, cyclesMaxFlag); err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
}
Loading