Exercise: Time Calculator
Difficulty - Beginner
Learning Objectives
- Master the time package in Go
- Work with time.Time and time.Duration
- Parse and format dates and times
- Calculate time differences and intervals
- Handle time zones correctly
Problem Statement
Create a TimeUtils package that implements common time calculations and formatting operations:
- DaysBetween: Calculate the number of days between two dates
- AddBusinessDays: Add business days to a date
- FormatDuration: Format a duration in human-readable format
- ParseFlexible: Parse dates from multiple common formats
- IsBusinessDay: Check if a date is a business day
Function Signatures
1package timeutils
2
3import "time"
4
5// DaysBetween returns the number of days between two dates
6func DaysBetween(start, end time.Time) int
7
8// AddBusinessDays adds n business days to the given date
9func AddBusinessDays(date time.Time, days int) time.Time
10
11// FormatDuration formats a duration in human-readable format
12// Example: "2 hours 30 minutes", "1 day 5 hours"
13func FormatDuration(d time.Duration) string
14
15// ParseFlexible attempts to parse a date from multiple formats
16func ParseFlexible(dateStr string)
17
18// IsBusinessDay returns true if the date is a weekday
19func IsBusinessDay(date time.Time) bool
20
21// NextBusinessDay returns the next business day after the given date
22func NextBusinessDay(date time.Time) time.Time
Example Usage
1package main
2
3import (
4 "fmt"
5 "time"
6 "timeutils"
7)
8
9func main() {
10 // DaysBetween
11 start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
12 end := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
13 days := timeutils.DaysBetween(start, end)
14 fmt.Printf("Days between: %d\n", days) // 14
15
16 // AddBusinessDays
17 date := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC) // Monday
18 newDate := timeutils.AddBusinessDays(date, 5)
19 fmt.Printf("5 business days later: %s\n", newDate.Format("2006-01-02"))
20 // Output: 2024-01-22
21
22 // FormatDuration
23 duration := 2*time.Hour + 30*time.Minute
24 formatted := timeutils.FormatDuration(duration)
25 fmt.Println(formatted) // "2 hours 30 minutes"
26
27 longDuration := 25*time.Hour + 15*time.Minute
28 formatted2 := timeutils.FormatDuration(longDuration)
29 fmt.Println(formatted2) // "1 day 1 hour 15 minutes"
30
31 // ParseFlexible
32 dates := []string{
33 "2024-01-15",
34 "01/15/2024",
35 "15-Jan-2024",
36 "January 15, 2024",
37 }
38
39 for _, dateStr := range dates {
40 parsed, err := timeutils.ParseFlexible(dateStr)
41 if err != nil {
42 fmt.Printf("Failed to parse %s: %v\n", dateStr, err)
43 } else {
44 fmt.Printf("Parsed %s: %s\n", dateStr, parsed.Format("2006-01-02"))
45 }
46 }
47
48 // IsBusinessDay
49 monday := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
50 saturday := time.Date(2024, 1, 13, 0, 0, 0, 0, time.UTC)
51
52 fmt.Printf("Monday is business day: %v\n", timeutils.IsBusinessDay(monday)) // true
53 fmt.Printf("Saturday is business day: %v\n", timeutils.IsBusinessDay(saturday)) // false
54
55 // NextBusinessDay
56 friday := time.Date(2024, 1, 19, 0, 0, 0, 0, time.UTC)
57 nextDay := timeutils.NextBusinessDay(friday)
58 fmt.Printf("Next business day after Friday: %s\n", nextDay.Format("Monday"))
59 // Output: Monday
60}
Requirements
- DaysBetween should return absolute value
- AddBusinessDays should skip weekends
- FormatDuration should handle days, hours, minutes, and seconds
- ParseFlexible should support at least 4 common date formats
- IsBusinessDay should only consider Monday-Friday
Test Cases
1package timeutils
2
3import (
4 "testing"
5 "time"
6)
7
8func TestDaysBetween(t *testing.T) {
9 tests := []struct {
10 start time.Time
11 end time.Time
12 expected int
13 }{
14 {
15 time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
16 time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
17 14,
18 },
19 {
20 time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
21 time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
22 14, // Should be positive
23 },
24 {
25 time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
26 time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
27 0,
28 },
29 }
30
31 for _, tt := range tests {
32 result := DaysBetween(tt.start, tt.end)
33 if result != tt.expected {
34 t.Errorf("DaysBetween(%v, %v) = %d; want %d",
35 tt.start.Format("2006-01-02"),
36 tt.end.Format("2006-01-02"),
37 result, tt.expected)
38 }
39 }
40}
41
42func TestAddBusinessDays(t *testing.T) {
43 // Monday, January 15, 2024
44 monday := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
45
46 tests := []struct {
47 start time.Time
48 days int
49 expected time.Time
50 }{
51 {monday, 1, time.Date(2024, 1, 16, 0, 0, 0, 0, time.UTC)}, // Tuesday
52 {monday, 5, time.Date(2024, 1, 22, 0, 0, 0, 0, time.UTC)}, // Next Monday
53 {monday, 0, monday}, // Same day
54 }
55
56 for _, tt := range tests {
57 result := AddBusinessDays(tt.start, tt.days)
58 if !result.Equal(tt.expected) {
59 t.Errorf("AddBusinessDays(%v, %d) = %v; want %v",
60 tt.start.Format("2006-01-02"), tt.days,
61 result.Format("2006-01-02"), tt.expected.Format("2006-01-02"))
62 }
63 }
64}
65
66func TestFormatDuration(t *testing.T) {
67 tests := []struct {
68 duration time.Duration
69 expected string
70 }{
71 {2 * time.Hour, "2 hours"},
72 {30 * time.Minute, "30 minutes"},
73 {2*time.Hour + 30*time.Minute, "2 hours 30 minutes"},
74 {25 * time.Hour, "1 day 1 hour"},
75 {90 * time.Second, "1 minute 30 seconds"},
76 }
77
78 for _, tt := range tests {
79 result := FormatDuration(tt.duration)
80 if result != tt.expected {
81 t.Errorf("FormatDuration(%v) = %q; want %q", tt.duration, result, tt.expected)
82 }
83 }
84}
85
86func TestIsBusinessDay(t *testing.T) {
87 tests := []struct {
88 date time.Time
89 expected bool
90 }{
91 {time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC), true}, // Monday
92 {time.Date(2024, 1, 16, 0, 0, 0, 0, time.UTC), true}, // Tuesday
93 {time.Date(2024, 1, 19, 0, 0, 0, 0, time.UTC), true}, // Friday
94 {time.Date(2024, 1, 13, 0, 0, 0, 0, time.UTC), false}, // Saturday
95 {time.Date(2024, 1, 14, 0, 0, 0, 0, time.UTC), false}, // Sunday
96 }
97
98 for _, tt := range tests {
99 result := IsBusinessDay(tt.date)
100 if result != tt.expected {
101 t.Errorf("IsBusinessDay(%v) = %v; want %v",
102 tt.date.Format("Monday"), result, tt.expected)
103 }
104 }
105}
Hints
Hint 1: DaysBetween
Use time.Since() or time.Until() to get the duration, then convert to days. Use math.Abs() to ensure positive result.
1duration := end.Sub(start)
2days := int(math.Abs(duration.Hours() / 24))
Hint 2: AddBusinessDays
Loop and add one day at a time, skipping weekends. Check date.Weekday() to determine if it's Saturday or Sunday.
Hint 3: FormatDuration
Extract days, hours, minutes, and seconds from the duration. Build a string with only non-zero components.
1days := int(d.Hours() / 24)
2hours := int(d.Hours()) % 24
3minutes := int(d.Minutes()) % 60
4seconds := int(d.Seconds()) % 60
Hint 4: ParseFlexible
Try parsing with multiple layouts using time.Parse(). Return the first successful parse.
1layouts := []string{
2 "2006-01-02",
3 "01/02/2006",
4 "02-Jan-2006",
5 "January 2, 2006",
6}
Hint 5: IsBusinessDay
Check if date.Weekday() is between Monday and Friday.
Solution
Click to see the complete solution
1package timeutils
2
3import (
4 "fmt"
5 "math"
6 "strings"
7 "time"
8)
9
10// DaysBetween returns the number of days between two dates
11func DaysBetween(start, end time.Time) int {
12 duration := end.Sub(start)
13 days := int(math.Abs(duration.Hours() / 24))
14 return days
15}
16
17// AddBusinessDays adds n business days to the given date
18func AddBusinessDays(date time.Time, days int) time.Time {
19 current := date
20 remaining := days
21
22 for remaining > 0 {
23 current = current.AddDate(0, 0, 1)
24
25 // Skip weekends
26 if current.Weekday() != time.Saturday && current.Weekday() != time.Sunday {
27 remaining--
28 }
29 }
30
31 return current
32}
33
34// FormatDuration formats a duration in human-readable format
35func FormatDuration(d time.Duration) string {
36 if d == 0 {
37 return "0 seconds"
38 }
39
40 // Extract components
41 totalSeconds := int(d.Seconds())
42
43 days := totalSeconds / 86400
44 totalSeconds %= 86400
45
46 hours := totalSeconds / 3600
47 totalSeconds %= 3600
48
49 minutes := totalSeconds / 60
50 seconds := totalSeconds % 60
51
52 // Build result string
53 var parts []string
54
55 if days > 0 {
56 if days == 1 {
57 parts = append(parts, "1 day")
58 } else {
59 parts = append(parts, fmt.Sprintf("%d days", days))
60 }
61 }
62
63 if hours > 0 {
64 if hours == 1 {
65 parts = append(parts, "1 hour")
66 } else {
67 parts = append(parts, fmt.Sprintf("%d hours", hours))
68 }
69 }
70
71 if minutes > 0 {
72 if minutes == 1 {
73 parts = append(parts, "1 minute")
74 } else {
75 parts = append(parts, fmt.Sprintf("%d minutes", minutes))
76 }
77 }
78
79 if seconds > 0 {
80 if seconds == 1 {
81 parts = append(parts, "1 second")
82 } else {
83 parts = append(parts, fmt.Sprintf("%d seconds", seconds))
84 }
85 }
86
87 return strings.Join(parts, " ")
88}
89
90// ParseFlexible attempts to parse a date from multiple formats
91func ParseFlexible(dateStr string) {
92 layouts := []string{
93 "2006-01-02",
94 "01/02/2006",
95 "02-Jan-2006",
96 "January 2, 2006",
97 time.RFC3339,
98 time.RFC822,
99 }
100
101 for _, layout := range layouts {
102 if t, err := time.Parse(layout, dateStr); err == nil {
103 return t, nil
104 }
105 }
106
107 return time.Time{}, fmt.Errorf("unable to parse date: %s", dateStr)
108}
109
110// IsBusinessDay returns true if the date is a weekday
111func IsBusinessDay(date time.Time) bool {
112 weekday := date.Weekday()
113 return weekday >= time.Monday && weekday <= time.Friday
114}
115
116// NextBusinessDay returns the next business day after the given date
117func NextBusinessDay(date time.Time) time.Time {
118 next := date.AddDate(0, 0, 1)
119
120 for !IsBusinessDay(next) {
121 next = next.AddDate(0, 0, 1)
122 }
123
124 return next
125}
Explanation
DaysBetween:
- Uses
Sub()to get duration between dates - Converts to hours then divides by 24 for days
- Uses
math.Abs()to ensure positive result - Note: This is a simplified version; production code might use
date.Sub(date)for more accuracy
AddBusinessDays:
- Iterates day by day, checking if each day is a business day
- Uses
Weekday()to check for Saturday/Sunday - Only decrements counter on business days
- Time complexity: O(n) where n is number of days
FormatDuration:
- Extracts days, hours, minutes, seconds from total seconds
- Uses modulo arithmetic to get remainders
- Builds array of non-zero components
- Handles singular vs plural properly
ParseFlexible:
- Tries multiple common date layouts
- Returns first successful parse
- Returns error if all formats fail
- Can be extended with more formats
IsBusinessDay:
- Simple weekday check
- Monday through Friday are business days
- Saturday and Sunday are not
- Note: Doesn't account for holidays
NextBusinessDay:
- Adds one day and checks if it's a business day
- Continues adding days until business day found
- Useful for calculating due dates
Time Package Best Practices
1. Always use time.Time for dates:
1// Good
2start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
3
4// Avoid using strings for dates internally
2. Be aware of time zones:
1// UTC for storage/comparison
2utcTime := time.Now().UTC()
3
4// Local time for display
5localTime := utcTime.Local()
3. Use time.Duration for intervals:
1// Good
2timeout := 30 * time.Second
3
4// Avoid
5timeout := 30 // Ambiguous - seconds? milliseconds?
4. Comparing times:
1// Use Equal() for exact comparison
2if t1.Equal(t2) { }
3
4// Use Before() and After() for ordering
5if t1.Before(t2) { }
Bonus Challenges
- Business Hours Calculator: Calculate business hours between two timestamps
1func BusinessHoursBetween(start, end time.Time) float64
2// Assumes 9 AM - 5 PM business hours
- Holiday Support: Extend AddBusinessDays to skip holidays
1func AddBusinessDaysWithHolidays(date time.Time, days int, holidays []time.Time) time.Time
- Recurring Events: Calculate next occurrence of a recurring event
1type Recurrence string
2
3const (
4 Daily Recurrence = "daily"
5 Weekly Recurrence = "weekly"
6 Monthly Recurrence = "monthly"
7)
8
9func NextOccurrence(start time.Time, recurrence Recurrence) time.Time
- Age Calculator: Calculate age in years, months, and days
1type Age struct {
2 Years int
3 Months int
4 Days int
5}
6
7func CalculateAge(birthDate time.Time) Age
Key Takeaways
- time.Time is the standard for representing dates and times
- time.Duration represents intervals with nanosecond precision
- Always specify time zones explicitly
- Use time.Parse() and Format() with layout strings
- The reference time is "Mon Jan 2 15:04:05 MST 2006" for layouts
- Weekday() returns time.Weekday
Time handling is crucial for many applications. Go's time package is powerful but requires understanding of layouts, time zones, and durations. Master these concepts to handle time-based logic correctly in your applications.