diff --git a/cmd/dayOff.go b/cmd/dayOff.go new file mode 100644 index 0000000..262666e --- /dev/null +++ b/cmd/dayOff.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "errors" + "fmt" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const dFormat = "2006-01-02" + +// dayOffCmd represents the dayOff command +var dayOffCmd = &cobra.Command{ + Use: "dayOff [day]", + Short: "Add a day that isn't calcualted for total. (format: 2016-04-28)", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("requires a day (format: 2016-04-28)") + } + if validDay(args[0]) { + return nil + } + return fmt.Errorf("invalid day format (yyyy-mm-dd): %s", args[0]) + }, + Run: func(cmd *cobra.Command, args []string) { + addDayOff(args[0]) + }, +} + +func init() { + rootCmd.AddCommand(dayOffCmd) +} + +func validDay(day string) bool { + _, err := time.Parse(dFormat, day) + if err != nil { + return false + } + return true +} + +func addDayOff(day string) { + t, _ := time.Parse(dFormat, day) + newDay := t.Format(dFormat) + days := viper.GetStringSlice("days_off") + for _, d := range days { + if d == newDay { + fmt.Println(newDay, "already exists") + return + } + } + days = append(days, newDay) + + viper.Set("days_off", days) + if err := viper.WriteConfig(); err != nil { + fmt.Println("problem with updating config file", err) + } + return +} diff --git a/cmd/report.go b/cmd/report.go index 0e7c605..cfde8a9 100644 --- a/cmd/report.go +++ b/cmd/report.go @@ -39,9 +39,11 @@ func runReport(format string) { panic(err) } filename := viper.GetString("tracking_file") + daysOff := viper.GetStringSlice("days_off") fmt.Printf("hours worked this week: %.1f \n", reporting.HoursWorkedThisWeek(filename, user.Username)) - fmt.Printf("hours worked this month: %.1f \n", reporting.HoursWorkedThisMonth(filename, user.Username)) + fmt.Printf("hours worked this month: %.1f \n", reporting.HoursWorkedThisMonth(filename, user.Username, daysOff)) + fmt.Printf(" out of possible days: %d \n", reporting.AvailableDaysThisMonth(time.Now(), daysOff)) fmt.Println("-------------") fmt.Println(reporting.TextCalendar(time.Now(), filename, user.Username)) diff --git a/pkg/reporting/simple.go b/pkg/reporting/simple.go index 20a42cb..b355342 100644 --- a/pkg/reporting/simple.go +++ b/pkg/reporting/simple.go @@ -1,6 +1,7 @@ package reporting import ( + "fmt" "strconv" "time" ) @@ -32,22 +33,56 @@ func sumThisWeek(data Years, t time.Time) float64 { } // HoursWorkedThisMonth returns the total hours documented per day for the current month -func HoursWorkedThisMonth(filename, user string) float64 { +func HoursWorkedThisMonth(filename, user string, daysOff []string) float64 { data := getTrackedData(filename)[user] now := time.Now() - return sumThisMonth(data, now) + return sumThisMonth(data, now, daysOff) } -func sumThisMonth(data Years, t time.Time) float64 { +func sumThisMonth(data Years, t time.Time, daysOff []string) float64 { thisDay := t y := strconv.Itoa(thisDay.Year()) m := thisDay.Month().String() // uses month name - might "break" if user switches locales days := data[y][m] sum := 0 - for _, d := range days { - sum += d + for key, d := range days { + day, _ := strconv.Atoi(key) + if !isDayOff(daysOff, fmt.Sprintf("%s-%02d-%02d", y, int(thisDay.Month()), day)) { + sum += d + } } return float64(sum) / 60 } + +// AvailableDaysThisMonth returns the total days that could be working days +func AvailableDaysThisMonth(t time.Time, daysOff []string) int { + totalDays := 0 + thisDay := t + lastDay := thisDay.Day() + // lastDayMonth := time.Date(thisDay.Year(), time.Month(int(thisDay.Month())+1), 0, 0, 0, 0, 0, time.UTC) + // lastDay := lastDayMonth.Day() + + for i := 1; i <= lastDay; i++ { + currDay := time.Date(thisDay.Year(), time.Month(int(thisDay.Month())), i, 0, 0, 0, 0, time.UTC) + if isDayOff(daysOff, currDay.Format("2006-01-02")) { + continue + } + if int(currDay.Weekday()) == 0 || int(currDay.Weekday()) == 6 { + continue + } + totalDays++ + } + + return totalDays +} + +func isDayOff(daysOff []string, day string) bool { + for _, d := range daysOff { + if d == day { + return true + } + } + return false +} diff --git a/pkg/reporting/simple_test.go b/pkg/reporting/simple_test.go index ae069be..a8d5413 100644 --- a/pkg/reporting/simple_test.go +++ b/pkg/reporting/simple_test.go @@ -55,6 +55,7 @@ func Test_sumThisMonth(t *testing.T) { name string data Years t time.Time + off []string want float64 }{ { @@ -87,12 +88,54 @@ func Test_sumThisMonth(t *testing.T) { t: dec, want: 660 / 60, }, + { + name: "two days, one off", + data: Years{"2020": {"December": {"30": 1440, "31": 1443}}}, + t: dec, + off: []string{"2020-12-31"}, + want: 1440.0 / 60, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := sumThisMonth(tt.data, tt.t); got != tt.want { + if got := sumThisMonth(tt.data, tt.t, tt.off); got != tt.want { t.Errorf("sumThisMonth() = %v, want %v", got, tt.want) } }) } } + +func Test_workingDays(t *testing.T) { + dec, _ := time.Parse("2006-01-02 15:04", "2020-12-31 9:59") + jan, _ := time.Parse("2006-01-02 15:04", "2021-01-02 9:59") + tests := []struct { + name string + t time.Time + off []string + want int + }{ + { + name: "last day of month", + t: dec, + want: 23, + }, + { + name: "2nd day of month is Saturday", + t: jan, + want: 1, + }, + { + name: "last day of month, two days off", + t: dec, + off: []string{"2020-12-25", "2020-12-31"}, + want: 21, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := AvailableDaysThisMonth(tt.t, tt.off); got != tt.want { + t.Errorf("workingDays() = %v, want %v", got, tt.want) + } + }) + } +}