Gowhere – array, hash table, regular expression, and foreach loop

Gowhere? Some steps further—append arrays, hash table, regular expression, and for each loop.

After playing around with HTTP, I got the JSON data from the API. I wanted to analyze the data—to display the total hours spent on each work item.

A typical record has this structure

type WorkItem struct {
    FromDateTime time.Time
    ToDateTime   time.Time
    Detail       string
}

And an actual work item looks like

{
    "FromDateTime" : "2019-11-29 02:05:00 +0000 UTC",
    "ToDateTime": "2019-11-29 03:19:00 +0000 UTC",
    "Detail" : "Work Item 12345: Implement a cool feature"
}

The "Work Item 12345" has many records. The detail field are not the same except they contain the number 12345 as the work item ID. I want to display the sum of time spent for Work Item 12345. So the algorithm is pretty simple

  1. For each record, extract the work item id from the detail field
  2. Calculate the difference in hours between FromDateTime and ToDateTime
  3. Sum the difference with the existing value—if not exist, create a new one with the time spent zero

Note: If I am writing in C#, I can finish the implementation quickly with Linq support.

The expected result of a work item is below

{
    "WorkItemId" : "12345",
    "WorkItemName" : "Work Item 12345: Implement a cool feature",
    "TimeSpent" : time_spent_in_hours
}

Stats structure to hold the analysis result of a work item

// Stats ...
type Stats struct {
    WorkItemId    string
    WorkItemName  string
    TimeSpent float64
}

Let’s write some code and explore

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "regexp"
    "time"
)

func main() {
    // Assuming that I have a list of IDs for a team.
    // The getWorkItemRecords will an array of WorkItem of a member.
    var records = make([]WorkItem, 0)
    for _, id := range team {
        r := getWorkItemRecords(id)
        // Discussion about appending 2 slices: https://stackoverflow.com/questions/16248241/concatenate-two-slices-in-go

        // This is how to append 2 arrays
        records = append(records, r...)
    }

    // A dictionary (hash table) with
    // key: WorkItemID (or name if cannot find the ID) - simply a string
    // value: total time spent
    // More detail about map here: https://blog.golang.org/go-maps-in-action
    statsMap := make(map[string]Stats)

    // Regular expression to extract ID (all numeric characters)
    workItemIdExp := regexp.MustCompile("[\\d]+")

    var id string

    for _, r := range records {
        timeSpent := r.ToDateTime.Sub(r.FromDateTime).Hours()
        if timeSpent < 0 {
            // The record does not have an end time
            continue
        }

        id = workItemIdExp.FindString(r.Detail)

        if id == "" {
            id = r.Detail
        }

        ts, exist := statsMap[id]

        if !exist {
            ts = Stats{id, r.Detail, 0}
        }
        ts.TimeSpent += timeSpent
        statsMap[id] = ts
    }

    var workingHours float64 = 0
    for key, value := range statsMap {
        workingHours += value.TimeSpent
        fmt.Printf("%s (%s) %f\n", key, value.WorkItemName, value.TimeSpent)
    }

    fmt.Printf("Working hours: %f\n", workingHours)
}

What are my further steps from this exercise?

  1. Append 2 arrays (slices) with "…" syntax—append(records, r...)
  2. Hash table (dictionary like) with mapmap[string]int means a dictionary with key is a string and value is an integer
  3. Regular expression with regexp package—regexp.MustCompile("[\\d]+")
  4. For each loop with the rangefor _, r := range records

A happy weekend!

Gowhere – http

I Go (went) a step further—http and other things to consume an API service. What would it take to call an API which returns a list of records in JSON? In .NET, it takes a few lines of code.

Scenario: Display a list of employees—Id, First Name, Last Name, and Joined Date—from a protected API—of a company. The returned value might contain more fields than necessary.

  1. Id: integer
  2. First Name and Last Name: string
  3. Joined Date: Date Time

First thing first, create a file http.go and write some code. To work with http, Go supplies the net/http package.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    fmt.Println("Connecting to the API ...")
    const url = "https://xxxcompany.com/api/employees"

    const accessToken = "base64 access token"

    fmt.Println("Base Address: ", url)
    fmt.Println("Access Token: ", accessToken)

    // Issue a default request but will not work because of the missing access token
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println("Cannot connect the API: ", err)
        return
    }

    // Close the body at the end of the execution
    defer resp.Body.Close()
}

Nothing’s fancy! I took them from the Go http package. When invoking a HTTP call, Go returns a response with an error if there is a connection problem. Go suggests that we must always check for error before usage—a good practice.

The above code will return a 401 status code—Not Authorized. I need to attach the access token to the request. To manipulate the request, I need to create it by myself and ask Go to send it. It is quite easy.

    // Create a custom request with custom headers
    resq, err := http.NewRequest(http.MethodGet, url, nil)
    resq.Header.Add("x-access-token", accessToken)
    // Send the request using the default client supplied by the http
    resp, err := http.DefaultClient.Do(resq)

    if err != nil {
        fmt.Println("Cannot connect the API: ", err)
        return
    }

What did I get from resp.Body? A binary stream.

binary, error:= ioutil.RealAll(resp.Body)

But I need a list of employees which is in JSON format. Go gives me the encoding/json package to decode from binary, represented data in JSON format, to object—struct in Go. So I define an Employee struct—custom data type—to hold the result. The Employee struct has JoinedDate which is a date time—time package is supplied to deal with time.

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

// Existing code

// Employee
type Employee struct {
    Id         int
    FirstName  string
    LastName   string
    JoinedDate time.Time
}

And it’s time for gardening—decode the binary stream into list of employees

    // Create a decoder with passing the io reader from resp.Body
    decoder := json.NewDecoder(resp.Body)
    // Prepare an empty array of employees
    employees := make([]Employee, 0)
    // Decode, pass the pointer to the employees
    decoder.Decode(&employees)
    // Print the result
    fmt.Println(employees)

Put them all together, I have a working program. Run go run http.go and feel good.

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

func main() {
    fmt.Println("Connecting to the API ...")
    const url = "https://xxxcompany.com/api/employees"

    const accessToken = "base64 access token"

    fmt.Println("Base Address: ", url)
    fmt.Println("Access Token: ", accessToken)

    // Create a custom request with custom headers
    resq, err := http.NewRequest(http.MethodGet, url, nil)
    resq.Header.Add("x-access-token", accessToken)
    // Send the request using the default client supplied by the http
    resp, err := http.DefaultClient.Do(resq)

    if err != nil {
        fmt.Println("Cannot connect the API: ", err)
        return
    }

    // Close the body at the end of the execution
    defer resp.Body.Close()

    // Create a decoder with passing the io reader from resp.Body
    decoder := json.NewDecoder(resp.Body)
    // Prepare an empty array of employees
    employees := make([]Employee, 0)
    // Decode, pass the pointer to the employees
    decoder.Decode(&employees)
    // Print the result
    fmt.Println(employees)
}

// Employee
type Employee struct {
    Id         int
    FirstName  string
    LastName   string
    JoinedDate time.Time
}

Go where? One step further.

  1. 3 new packages: net/http, encoding/json, time
  2. Create custom http requests
  3. Define a new type via struct—class in C#
  4. Decode from binary stream—JSON data—to an array of object
  5. Use make method to create an object from a type

It makes my day, especially for the weekend!