Welcome to part three of a series of articles where we create a pomodoro timer as a way to learn Golang.
In the previous article we added a system notification that would work regardless of what operating system you were on, and it would speak to you if you were on a Mac.
I've had a few meetings now where it's suddenly declared “Get back to work” followed by an awkward silence where everyone looks at me.
I think I need to fix that…
I had thought that my next improvement to the pomodoro timer was going to be adding a GUI. That was a “nice to have”, this is definitely a “need”.
Currently I'm quite fortunate in that I only have two regular meetings a day, and I know what time they're at. So my next upgrade is going to be the ability to add “quiet times” where the visual notification will still show, but there won't be any awkward audible alerts.
Let's keep it simple for now and just define some times in a json file that the program can read. I have fifteen minutes until my next regular meeting, so let's get to it so we can test it!
Here are the times I want to be quiet. Sorry for any Americans reading — 24 hour clock just makes so much more sense.
// quietTimes.json
[
{
"start": "11:00",
"end": "11:30"
},
{
"start": "14:30",
"end": "15:00"
}
]
In the previous update we added a conditional to check if we were on a mac before trying to use the say
api. We're going to add !isQuietTime()
to the conditional here:
if os == "darwin" && !isQuietTime() {
go exec.Command("say", message).Output()
}
Now all we need to do is implement this function! We just need to read our json and compare the current time to the time in the json file.
The steps are pretty much the same as in any programming language. We want to:
Opening the file is easy and straightforward. We'll handle the error incase there are any issues (like misspelling the filename — doh!).
jsonFile, err := os.Open("quietTimes.json")
if err != nil {
fmt.Println(err)
}
fmt.Println("Successfully Opened quietTimes.json")
Next we can defer
closing the file before we forget about it. Using defer
means that Go will wait until the surrounding function returns before executing.
defer jsonFile.Close()
Calling it before we do anything else means that if we have an error after this point, the file should still be closed.
In order to assign the contents to a variable, we need to read the file and get the byte slice value:
byteValue, _ := ioutil.ReadAll(jsonFile)
Now for the interesting part… The json package in Go has a method called Unmarshal
which allows us to define an interface which will be used to understand (parse) the contents of the file.
We know that our JSON file is basically a list of start and end times, so we can define our interface like this:
type quietTimes []quietTime
type quietTime struct {
Start string
End string
}
Then by calling the Unmarshal
function we should get a slice of quietTime
structs. If you're paying good attention you may notice that our json keys are lowercase whereas the keys of the struct start with a capital letter. The function is clever enough that it can match Start
with start
or even StArT
.
var quietTimes quietTimes
json.Unmarshal(byteValue, &quietTimes)
The second argument to Unmarshal
needs to be a pointer, so we're using the &
notation. A pointer in Go is a reference to where the value is stored in memory. Using &
will give us the memory location instead of the actual value itself. As a quick aside, you can run this code on play.golang.org :
a := "test"
fmt.Println(a) // test
fmt.Println(&a) // 0xc00009e210
Back to our program, if we print out quietTimes, it gives us our data as expected:
fmt.Printf("\n%+v\n", quietTimes)
// [{Start:11:00 End:11:30} {Start:14:30 End:15:00}]
We're doing some interesting string formatting here. \n
simply creates a new line so that we can see our message, and %+v
is well described in the docs:
%v the value in a default format
when printing structs, the plus flag (%+v) adds field names
Now that we have a Go slice with our data in it, we should be able to do a time comparison to see if we're in the quiet time window or not.
First we need to convert our string
to a Time
. Since we've got our time in a standard format of HH:MM
, we can split the string to get the hour and minute values, and then use those to build a new Time
.
// quietTimes.go
// s = "11:30"
func getTimeFromString(s string) time.Time {
ct := time.Now()
t := strings.Split(s, ":")
hour, _ := strconv.Atoi(t[0])
min, _ := strconv.Atoi(t[1])
return time.Date(ct.Year(), ct.Month(), ct.Day(), hour, min, 0, 0, ct.Location())
}
This method simply converts the string to two variables hour
and min
and then uses that in the standard construction of a new Time
.
Now we can use some really nice Time functions in Go to check if we're in the right time window:
// quietTimes.go
ct := time.Now()
isQuietTime := false
for _, quietTime := range quietTimes {
quietTimeStart := getTimeFromString(quietTime.Start)
quietTimeEnd := getTimeFromString(quietTime.End)
if ct.After(quietTimeStart) && ct.Before(quietTimeEnd) {
isQuietTime = true
}
}
I really like the Go Time package. It's really easy and intuitive to understand, which is great considering the pain I've had in JavaScript working with time.
That's us done - It works! No more awkward meetings for me! Here's a view of the full quietTimes.go file that we created in this article.
// quietTimes.go
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"time"
)
type quietTimes []quietTime
type quietTime struct {
Start string
End string
}
func isQuietTime() bool {
jsonFile, err := os.Open("quietTimes.json")
if err != nil {
fmt.Println(err)
}
fmt.Println("Successfully Opened quietTimes.json")
defer jsonFile.Close()
byteValue, _ := ioutil.ReadAll(jsonFile)
var quietTimes quietTimes
json.Unmarshal(byteValue, &quietTimes)
fmt.Printf("\n%+v\n", quietTimes)
ct := time.Now()
isQuietTime := false
for _, quietTime := range quietTimes {
quietTimeStart := getTimeFromString(quietTime.Start)
quietTimeEnd := getTimeFromString(quietTime.End)
if ct.After(quietTimeStart) && ct.Before(quietTimeEnd) {
isQuietTime = true
}
}
fmt.Println(isQuietTime)
return isQuietTime
}
func getTimeFromString(s string) time.Time {
ct := time.Now()
t := strings.Split(s, ":")
hour, _ := strconv.Atoi(t[0])
min, _ := strconv.Atoi(t[1])
return time.Date(ct.Year(), ct.Month(), ct.Day(), hour, min, 0, 0, ct.Location())
}
If you're following along with the pomodoro series, you can find the code at this point in time on GitHub.
Previous item in series:
In this article I highlight two main methods I found for creating a system alert using Go.
Next item in series:
Follow along as we take our simple pomodoro timer and make it a graphical application in a few simple steps
Self taught software developer with 11 years experience excelling at JavaScript/Typescript, React, Node and AWS.
I love learning and teaching and have mentored several junior developers over my career. I find teaching is one of the best ways to solidify your own learning, so in the past few years I've been maintaining a technical blog where I write about some things that I've been learning.
I'm passionate about building a teams culture and processes to make it efficient and satisfying to work in. In many roles I have improved the quality and reliability of the code base by introducing or improving the continuous integration pipeline to include quality gates.