diff --git a/pkg/web/html/overtimereport_view.go b/pkg/web/html/overtimereport_view.go
index a7664cc..4329dab 100644
--- a/pkg/web/html/overtimereport_view.go
+++ b/pkg/web/html/overtimereport_view.go
@@ -26,7 +26,7 @@ func (v *OvertimeReportView) formatDailySummary(daily *timesheet.DailySummary) V
basic := Values{
"Weekday": daily.Date.Weekday(),
"Date": daily.Date.Format(odoo.DateFormat),
- "OvertimeHours": strconv.FormatFloat(daily.CalculateOvertime().Hours(), 'f', 2, 64),
+ "OvertimeHours": formatDurationInHours(daily.CalculateOvertime()),
"LeaveType": "",
}
if daily.HasAbsences() {
@@ -35,9 +35,25 @@ func (v *OvertimeReportView) formatDailySummary(daily *timesheet.DailySummary) V
return basic
}
+// formatDurationInHours returns a human friendly "0:00"-formatted duration.
+// Seconds within a minute are rounded up or down to the nearest full minute.
+// A sign ("-") is prefixed if duration is negative.
+func formatDurationInHours(d time.Duration) string {
+ sign := ""
+ if d.Seconds() < 0 {
+ sign = "-"
+ d = time.Duration(d.Nanoseconds() * -1)
+ }
+ d = d.Round(time.Minute)
+ h := d / time.Hour
+ d -= h * time.Hour
+ m := d / time.Minute
+ return fmt.Sprintf("%s%d:%02d", sign, h, m)
+}
+
func (v *OvertimeReportView) formatSummary(s timesheet.Summary) Values {
return Values{
- "TotalOvertime": s.TotalOvertime.Truncate(time.Minute),
+ "TotalOvertime": formatDurationInHours(s.TotalOvertime.Truncate(time.Minute)),
// TODO: Might not be accurate for days before 2021
"TotalLeaves": fmt.Sprintf("%sd", strconv.FormatFloat(s.TotalLeaveDays.Hours()/8, 'f', 0, 64)),
}
diff --git a/pkg/web/html/overtimereport_view_test.go b/pkg/web/html/overtimereport_view_test.go
new file mode 100644
index 0000000..11518d8
--- /dev/null
+++ b/pkg/web/html/overtimereport_view_test.go
@@ -0,0 +1,85 @@
+package html
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestOvertimeReportView_formatDurationHumanFriendly(t *testing.T) {
+ tests := map[string]struct {
+ givenDuration time.Duration
+ expectedOutcome string
+ }{
+ "GivenNoDuration_ThenReturnZero": {
+ givenDuration: time.Duration(0),
+ expectedOutcome: "0:00",
+ },
+ "GivenPositiveDuration_WhenDurationMoreThan30s_ThenRoundUp": {
+ givenDuration: parseDuration(t, "30s"),
+ expectedOutcome: "0:01",
+ },
+ "GivenPositiveDuration_WhenDurationMoreThan30s_ThenRoundUpEdgeCase": {
+ givenDuration: parseDuration(t, "1m59s"),
+ expectedOutcome: "0:02",
+ },
+ "GivenPositiveDuration_WhenDurationLessThan30s_ThenRoundDown": {
+ givenDuration: parseDuration(t, "29s"),
+ expectedOutcome: "0:00",
+ },
+ "GivenPositiveDuration_WhenUnder1Hour_ThenReturnMinutesOnly": {
+ givenDuration: parseDuration(t, "38m"),
+ expectedOutcome: "0:38",
+ },
+ "GivenPositiveDuration_WhenOver1Hour_ThenReturnHoursAndMinutes": {
+ givenDuration: parseDuration(t, "1h38m"),
+ expectedOutcome: "1:38",
+ },
+ "GivenPositiveDuration_WhenOver10Hour_ThenReturnHoursAndMinutes": {
+ givenDuration: parseDuration(t, "10h38m"),
+ expectedOutcome: "10:38",
+ },
+ "GivenPositiveDuration_WhenOver100Hour_ThenReturnHoursAndMinutes": {
+ givenDuration: parseDuration(t, "100h38m"),
+ expectedOutcome: "100:38",
+ },
+ "GivenNegativeDuration_WhenDurationMoreThan30s_ThenRoundUp": {
+ givenDuration: parseDuration(t, "-30s"),
+ expectedOutcome: "-0:01",
+ },
+ "GivenNegativeDuration_WhenDurationLessThan30s_ThenRoundDown": {
+ givenDuration: parseDuration(t, "-29s"),
+ expectedOutcome: "-0:00",
+ },
+ "GivenNegativeDuration_WhenUnder1Hour_ThenReturnMinutesOnly": {
+ givenDuration: parseDuration(t, "-38m"),
+ expectedOutcome: "-0:38",
+ },
+ "GivenNegativeDuration_WhenOver1Hour_ThenReturnHoursAndMinutes": {
+ givenDuration: parseDuration(t, "-1h38m"),
+ expectedOutcome: "-1:38",
+ },
+ "GivenNegativeDuration_WhenOver10Hour_ThenReturnHoursAndMinutes": {
+ givenDuration: parseDuration(t, "-10h38m"),
+ expectedOutcome: "-10:38",
+ },
+ "GivenNegativeDuration_WhenOver100Hour_ThenReturnHoursAndMinutes": {
+ givenDuration: parseDuration(t, "-100h38m"),
+ expectedOutcome: "-100:38",
+ },
+ }
+ for name, tt := range tests {
+ t.Run(name, func(t *testing.T) {
+ result := formatDurationInHours(tt.givenDuration)
+ assert.Equal(t, tt.expectedOutcome, result)
+ })
+ }
+}
+
+func parseDuration(t *testing.T, format string) time.Duration {
+ d, err := time.ParseDuration(format)
+ require.NoError(t, err)
+ return d
+}