Time Calculator

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:

  1. DaysBetween: Calculate the number of days between two dates
  2. AddBusinessDays: Add business days to a date
  3. FormatDuration: Format a duration in human-readable format
  4. ParseFlexible: Parse dates from multiple common formats
  5. 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

  1. DaysBetween should return absolute value
  2. AddBusinessDays should skip weekends
  3. FormatDuration should handle days, hours, minutes, and seconds
  4. ParseFlexible should support at least 4 common date formats
  5. 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

  1. Business Hours Calculator: Calculate business hours between two timestamps
1func BusinessHoursBetween(start, end time.Time) float64
2// Assumes 9 AM - 5 PM business hours
  1. Holiday Support: Extend AddBusinessDays to skip holidays
1func AddBusinessDaysWithHolidays(date time.Time, days int, holidays []time.Time) time.Time
  1. 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
  1. 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.