Exercism v3 launches on Sept 1st 2021. Learn more! ๐Ÿš€๐Ÿš€๐Ÿš€
Avatar of rootulp

rootulp's solution

to Clock in the Go Track

Published at Jun 10 2020 · 0 comments
Instructions
Test suite
Solution

Implement a clock that handles times without dates.

You should be able to add and subtract minutes to it.

Two clocks that represent the same time should be equal to each other.

Coding the solution

Look for a stub file having the name clock.go and place your solution code in that file.

Running the tests

To run the tests run the command go test from within the exercise directory.

If the test suite contains benchmarks, you can run these with the --bench and --benchmem flags:

go test -v --bench . --benchmem

Keep in mind that each reviewer will run benchmarks on a different machine, with different specs, so the results from these benchmark tests may vary.

Further information

For more detailed information about the Go track, including how to get help if you're having trouble, please visit the exercism.io Go language page.

Source

Pairing session with Erin Drummond https://twitter.com/ebdrummond

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

cases_test.go

package clock

// Source: exercism/problem-specifications
// Commit: b344762 clock: Add test case for exactly negative sixty minutes.
// Problem Specifications Version: 2.4.0

// Create a new clock with an initial time
var timeTests = []struct {
	h, m int
	want string
}{
	{8, 0, "08:00"},        // on the hour
	{11, 9, "11:09"},       // past the hour
	{24, 0, "00:00"},       // midnight is zero hours
	{25, 0, "01:00"},       // hour rolls over
	{100, 0, "04:00"},      // hour rolls over continuously
	{1, 60, "02:00"},       // sixty minutes is next hour
	{0, 160, "02:40"},      // minutes roll over
	{0, 1723, "04:43"},     // minutes roll over continuously
	{25, 160, "03:40"},     // hour and minutes roll over
	{201, 3001, "11:01"},   // hour and minutes roll over continuously
	{72, 8640, "00:00"},    // hour and minutes roll over to exactly midnight
	{-1, 15, "23:15"},      // negative hour
	{-25, 0, "23:00"},      // negative hour rolls over
	{-91, 0, "05:00"},      // negative hour rolls over continuously
	{1, -40, "00:20"},      // negative minutes
	{1, -160, "22:20"},     // negative minutes roll over
	{1, -4820, "16:40"},    // negative minutes roll over continuously
	{2, -60, "01:00"},      // negative sixty minutes is previous hour
	{-25, -160, "20:20"},   // negative hour and minutes both roll over
	{-121, -5810, "22:10"}, // negative hour and minutes both roll over continuously
}

// Add minutes
var addTests = []struct {
	h, m, a int
	want    string
}{
	{10, 0, 3, "10:03"},    // add minutes
	{6, 41, 0, "06:41"},    // add no minutes
	{0, 45, 40, "01:25"},   // add to next hour
	{10, 0, 61, "11:01"},   // add more than one hour
	{0, 45, 160, "03:25"},  // add more than two hours with carry
	{23, 59, 2, "00:01"},   // add across midnight
	{5, 32, 1500, "06:32"}, // add more than one day (1500 min = 25 hrs)
	{1, 1, 3500, "11:21"},  // add more than two days
}

// Subtract minutes
var subtractTests = []struct {
	h, m, a int
	want    string
}{
	{10, 3, 3, "10:00"},    // subtract minutes
	{10, 3, 30, "09:33"},   // subtract to previous hour
	{10, 3, 70, "08:53"},   // subtract more than an hour
	{0, 3, 4, "23:59"},     // subtract across midnight
	{0, 0, 160, "21:20"},   // subtract more than two hours
	{6, 15, 160, "03:35"},  // subtract more than two hours with borrow
	{5, 32, 1500, "04:32"}, // subtract more than one day (1500 min = 25 hrs)
	{2, 20, 3000, "00:20"}, // subtract more than two days
}

// Compare two clocks for equality
type hm struct{ h, m int }

var eqTests = []struct {
	c1, c2 hm
	want   bool
}{
	// clocks with same time
	{
		hm{15, 37},
		hm{15, 37},
		true,
	},
	// clocks a minute apart
	{
		hm{15, 36},
		hm{15, 37},
		false,
	},
	// clocks an hour apart
	{
		hm{14, 37},
		hm{15, 37},
		false,
	},
	// clocks with hour overflow
	{
		hm{10, 37},
		hm{34, 37},
		true,
	},
	// clocks with hour overflow by several days
	{
		hm{3, 11},
		hm{99, 11},
		true,
	},
	// clocks with negative hour
	{
		hm{22, 40},
		hm{-2, 40},
		true,
	},
	// clocks with negative hour that wraps
	{
		hm{17, 3},
		hm{-31, 3},
		true,
	},
	// clocks with negative hour that wraps multiple times
	{
		hm{13, 49},
		hm{-83, 49},
		true,
	},
	// clocks with minute overflow
	{
		hm{0, 1},
		hm{0, 1441},
		true,
	},
	// clocks with minute overflow by several days
	{
		hm{2, 2},
		hm{2, 4322},
		true,
	},
	// clocks with negative minute
	{
		hm{2, 40},
		hm{3, -20},
		true,
	},
	// clocks with negative minute that wraps
	{
		hm{4, 10},
		hm{5, -1490},
		true,
	},
	// clocks with negative minute that wraps multiple times
	{
		hm{6, 15},
		hm{6, -4305},
		true,
	},
	// clocks with negative hours and minutes
	{
		hm{7, 32},
		hm{-12, -268},
		true,
	},
	// clocks with negative hours and minutes that wrap
	{
		hm{18, 7},
		hm{-54, -11513},
		true,
	},
	// full clock and zeroed clock
	{
		hm{24, 0},
		hm{0, 0},
		true,
	},
}

clock_test.go

package clock

import (
	"reflect"
	"strconv"
	"strings"
	"testing"
)

// Clock API:
//
// type Clock                      // define the clock type
// New(hour, minute int) Clock     // a "constructor"
// (Clock) String() string         // a "stringer"
// (Clock) Add(minutes int) Clock
// (Clock) Subtract(minutes int) Clock
//
// To satisfy the README requirement about clocks being equal, values of
// your Clock type need to work with the == operator. This means that if your
// New function returns a pointer rather than a value, your clocks will
// probably not work with ==.
//
// While the time.Time type in the standard library (https://golang.org/pkg/time/#Time)
// doesn't necessarily need to be used as a basis for your Clock type, it might
// help to look at how constructors there (Date and Now) return values rather
// than pointers. Note also how most time.Time methods have value receivers
// rather than pointer receivers.
//
// For some useful guidelines on when to use a value receiver or a pointer
// receiver see: https://github.com/golang/go/wiki/CodeReviewComments#receiver-type

func TestCreateClock(t *testing.T) {
	for _, n := range timeTests {
		if got := New(n.h, n.m); got.String() != n.want {
			t.Fatalf("New(%d, %d) = %q, want %q", n.h, n.m, got, n.want)
		}
	}
	t.Log(len(timeTests), "test cases")
}

func TestAddMinutes(t *testing.T) {
	for _, a := range addTests {
		if got := New(a.h, a.m).Add(a.a); got.String() != a.want {
			t.Fatalf("New(%d, %d).Add(%d) = %q, want %q",
				a.h, a.m, a.a, got, a.want)
		}
	}
	t.Log(len(addTests), "test cases")
}

func TestSubtractMinutes(t *testing.T) {
	for _, a := range subtractTests {
		if got := New(a.h, a.m).Subtract(a.a); got.String() != a.want {
			t.Fatalf("New(%d, %d).Subtract(%d) = %q, want %q",
				a.h, a.m, a.a, got, a.want)
		}
	}
	t.Log(len(subtractTests), "test cases")
}

func TestAddMinutesStringless(t *testing.T) {
	for _, a := range addTests {
		var wantHour, wantMin int
		split := strings.SplitN(a.want, ":", 2)
		if len(split) > 0 {
			wantHour, _ = strconv.Atoi(split[0])
		}
		if len(split) > 1 {
			wantMin, _ = strconv.Atoi(split[1])
		}
		want := New(wantHour, wantMin)
		if got := New(a.h, a.m).Add(a.a); !reflect.DeepEqual(got, want) {
			t.Fatalf("New(%d, %d).Add(%d) = %v, want %v",
				a.h, a.m, a.a, got, want)
		}
	}
	t.Log(len(addTests), "test cases")
}

func TestSubtractMinutesStringless(t *testing.T) {
	for _, a := range subtractTests {
		var wantHour, wantMin int
		split := strings.SplitN(a.want, ":", 2)
		if len(split) > 0 {
			wantHour, _ = strconv.Atoi(split[0])
		}
		if len(split) > 1 {
			wantMin, _ = strconv.Atoi(split[1])
		}
		want := New(wantHour, wantMin)
		if got := New(a.h, a.m).Subtract(a.a); !reflect.DeepEqual(got, want) {
			t.Fatalf("New(%d, %d).Subtract(%d) = %v, want %v",
				a.h, a.m, a.a, got, want)
		}
	}
	t.Log(len(subtractTests), "test cases")
}

func TestCompareClocks(t *testing.T) {
	for _, e := range eqTests {
		clock1 := New(e.c1.h, e.c1.m)
		clock2 := New(e.c2.h, e.c2.m)
		got := clock1 == clock2
		if got != e.want {
			t.Log("Clock1:", clock1)
			t.Log("Clock2:", clock2)
			t.Logf("Clock1 == Clock2 is %t, want %t", got, e.want)
			if reflect.DeepEqual(clock1, clock2) {
				t.Log("(Hint: see comments in clock_test.go.)")
			}
			t.FailNow()
		}
	}
	t.Log(len(eqTests), "test cases")
}

func TestAddAndCompare(t *testing.T) {
	clock1 := New(15, 45).Add(16)
	clock2 := New(16, 1)
	if !reflect.DeepEqual(clock1, clock2) {
		t.Errorf("clock.New(15,45).Add(16) differs from clock.New(16,1)")
	}
}

func TestSubtractAndCompare(t *testing.T) {
	clock1 := New(16, 1).Subtract(16)
	clock2 := New(15, 45)
	if !reflect.DeepEqual(clock1, clock2) {
		t.Errorf("clock.New(16,1).Subtract(16) differs from clock.New(15,45)")
	}
}

func BenchmarkAddMinutes(b *testing.B) {
	c := New(12, 0)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for _, a := range addTests {
			c.Add(a.a)
		}
	}
}

func BenchmarkSubtractMinutes(b *testing.B) {
	c := New(12, 0)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		for _, a := range subtractTests {
			c.Subtract(a.a)
		}
	}
}

func BenchmarkCreateClocks(b *testing.B) {
	for i := 0; i < b.N; i++ {
		for _, n := range timeTests {
			New(n.h, n.m)
		}
	}
}

example_clock_test.go

package clock

import "fmt"

func ExampleClock_new() {
	// a new clock
	clock1 := New(10, 30)
	fmt.Println(clock1.String())

	// Output: 10:30
}

func ExampleClock_Add() {
	// create a clock
	clock := New(10, 30)

	// add 30 minutes to it
	clock = clock.Add(30)
	fmt.Println(clock.String())

	// Output: 11:00
}

func ExampleClock_Subtract() {
	// create a clock
	clock := New(10, 30)

	// subtract an hour and a half from it
	clock = clock.Subtract(90)
	fmt.Println(clock.String())

	// Output: 09:00
}

func ExampleClock_compare() {
	// a new clock
	clock1 := New(10, 30)

	// a second clock, same as the first
	clock2 := New(10, 30)

	// are the clocks equal?
	fmt.Println(clock2 == clock1)

	// change the second clock
	clock2 = clock2.Add(30)

	// are the clocks equal now?
	fmt.Println(clock2 == clock1)

	// Output:
	// true
	// false
}
package clock

import (
	"fmt"
	"time"
)

// Clock defines a type for keeping time.
type Clock struct {
	hour int
	min  int
}

// New is a constructor to create instances of Clock.
func New(hour, min int) Clock {
	t := time.Date(0, 0, 0, hour, min, 0, 0, time.UTC)
	return Clock{hour: t.Hour(), min: t.Minute()}
}

// Add moves the clock forward by the provided number of minutes.
func (clock Clock) Add(minutes int) Clock {
	t := clock.time().Add(durationFromMinutes(minutes))
	return Clock{hour: t.Hour(), min: t.Minute()}
}

// Subtract moves the clock backward by the provided number of minutes.
func (clock Clock) Subtract(minutes int) Clock {
	t := clock.time().Add(-durationFromMinutes(minutes))
	return Clock{hour: t.Hour(), min: t.Minute()}
}

func (clock Clock) String() string {
	return clock.time().Format("15:04")
}

// time converts the clock to a time.Time instance.
func (clock Clock) time() time.Time {
	return time.Date(0, 0, 0, clock.hour, clock.min, 0, 0, time.UTC)
}

func durationFromMinutes(minutes int) time.Duration {
	d, _ := time.ParseDuration(fmt.Sprintf("%dm", minutes)) // WARNING: Ignoring errors
	return d
}

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other peopleโ€™s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?