Building a Time Tracking Progress Application

tnvmadhav

TnvMadhav⚡

Posted on November 14, 2020

Building a Time Tracking Progress Application

"Progress is being made..."

It would be really cool to stamp milestones for various types of progress. It could be anything from a projects, careers to vacations. Timestamps are often used to plot the beginning of something and the expected time for something yet to happen or develop. Timestamps can be put on anything to literally mark events into our calendar. It gives us humans clarity about what can and cannot be done in project/life management.

Breaking down processes into manageable tasks gives us a drive to reach each milestone. These milestones are referenced based on timestamps and this temporal labelling helps us manage better to improve the outcome(s).

Time flows until entropy is levelled and for us humans, time is something we use to tame ourselves by setting up calendars. But more often than not, we tend to underestimate the power of time.

"Time is always moving forward" ~ not John Doe

To better understand this power, I feel that being reminded about it helps us focus on the goals we set for ourselves.

With this thought, I tried to create a product that does just that.

Time Progress App

This product reminds a person about the progress made with respect to the current year. This product blurts out the % year progress that has been made until that point. This was created to remind and in turn possibly help one put themselves or their progress in perspective.

The program blurts out on Twitter at TimeProgressApp.

The Year has progressed:
[--------------------||----] 86%

— TimeProgressApp (@TimeProgressApp) November 10, 2020

Did you call me here to get preached?

No, Let's talk about what is happening inside.

The idea is to be as accurate as possible and that is,

Spit out the numbers with little to no latency at all.

I used Golang to manage information and Heroku for hosting

The core functionality is divided into the following steps:

  1. Get the progress %
  2. Is it something significant to be reminded about?

    Yes? Pass along the number

    No? Repeat

Get the progress %

The progress being made with a chosen time reference( example, a year) can be put as:

Progress % = (time taken to reach now) / (total time duration) x 100

This calculation is isolated into a function GetTimeProgress:

func GetTimeProgress() float64 {
    time_year_start := time.Date(time.Now().Year(), 1, 1, 0, 0, 0, 0, time.UTC) //Time progressed in Hours
    time_year_end := time.Date(time.Now().Year() + 1 , 1, 1, 0, 0, 0, 0, time.UTC) // Get total time in hours
    return (float64(time.Since(time_year_start).Round(time.Second))/ float64(time_year_end.Sub(time_year_start).Round(time.Second))) * 100 // Year Progress
}
Enter fullscreen mode Exit fullscreen mode

Is it something significant to be reminded about?

Now the calling function w.r.t GetTimeProgress can give back any value percentage like 1.0304 (%) or 63.2323 (%). It would be cleaner to output whole numbers but we need the values to be as accurate as possible .

Therefore, we can get the expected behaviour by sending whole numbers when they are returned and we want to capture every whole number.

We are calculating the Progress % based on seconds metric, therefore, we can expect GetTimeProgress function to return whole numbers without significant misses.

So asserting on the whole numbers is the way to go, so something like

GetTimeProgress() % 10 == 0

would work with no problems...

Ok, you know what, I am paranoid, let me consider the possibility of some latency and we get back x.00000003 , then the above method wouldn't help.

So let's fix it, I am allowing the latency to be More than 0 or less than 0.00004

This will result in something like :

answer := GetTimeProgress()
latency := answer - math.Floor(answer)
if latency >= 0 && latency < 0.000004 {
            // Pass along the progress %
    }
Enter fullscreen mode Exit fullscreen mode

We need to do this until the appropriate value is reached, therefore, we can loop this functionality in an indefinite for loop, we then get

for {
        answer := GetTimeProgress()
        latency := answer - math.Floor(answer)
        if latency > 0 && latency < 0.000004 {
            return answer
        }
    }
Enter fullscreen mode Exit fullscreen mode

let us give the CPU some time to catch a breath, but we don't want to miss the intended progress % value, so let's add a single second (significant for a CPU) sleep time.

The latency consideration does in fact help as a second of sleep can possibly be the cause for missing the capture of whole number progress %

for {
        answer := GetTimeProgress()
        latency := answer - math.Floor(answer)
        if latency > 0 && latency < 0.000004 {
            return answer
        }
        time.Sleep(time.Second * 1)
    }
Enter fullscreen mode Exit fullscreen mode

Let's wrap this in a function,

// Return the right data
func IsProgressed() float64 {
    for {
        answer := GetTimeProgress()
        latency := answer - math.Floor(answer)
        // When to return the data...
        if latency > 0 && latency < 0.000004 {
            return answer
        }
        time.Sleep(time.Second * 1)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that we have isolated the logic for calculating time progress (GetTimeProgress) & check for the progress significance (IsProgressed) , we can rely on calling IsProgressed which will return a number when acceptable progress is made.

Design:

Now, we have two concerns

  1. Make sure that the calling function for IsProgressed doesn't just exit after getting the number.
  2. Tight coupling of the 'calculating functionality' with 'handling functionality'

Say the we are using a third party API to send the progress data, it is better to isolate the API call logic from the calculating logic. The calculating logic is time sensitive (to seconds accuracy) and API calls when are network based would lead to some time overhead.

We can isolate the two functionality with the help of channels and goroutines

Design Flow

Goroutines are light weight threads and channels are messaging pipes (here blocking) can be used for communication between (concurrently running) go routines. The system is best explained with the above diagram.

The handling go routine (main function) is configured at the receiving end of the channel that waits indefinitely until the channel returns the types value (Progress %)

A Channel is created using:

c := make(chan float64)
Enter fullscreen mode Exit fullscreen mode

A channel can only take in values that are of a specific type (defined during declaration).

A goroutine can be created by just stating go keyword before a certain function call, this spawns a separate child routine which executes the function.

go Updated(c)
Enter fullscreen mode Exit fullscreen mode

Here the child (calculation goroutine) is used to calculate and return the progress % when encountered and produce it into the channel passed as an argument.

// calculating goroutine:

updateChan <- IsProgressed()
Enter fullscreen mode Exit fullscreen mode

When the channel is loaded with a value, at the receiving end, the main (handling goroutine) will read the value and move on to make the API call (if any)

// handling goroutine:

answer := <- c

// Handle the data received from the channel...
sampleApiCall(answer)
Enter fullscreen mode Exit fullscreen mode

Once the data is consumed from the channel, we then call Twitter API to tweet the progress data.

The Final Code looks like:

package main

import (
    "fmt"
    "time"
    "math"
)

// Long running data producer
func Updated(updateChan chan float64) float64 {
    for {
        updateChan <- IsProgressed()
        // After milestone is reached, sleep for some time to prevent sending same progress data multiple times...
        time.Sleep(time.Hour * 8)
    }
}

// Return the right data
func IsProgressed() float64 {
    for {
        answer := GetTimeProgress()
        latency := answer - math.Floor(answer)
        // When to return the data...
        if latency > 0 && latency < 0.000004 {
            return answer
        }
        time.Sleep(time.Second * 1)
    }
}

// Just give me the damn number!
func GetTimeProgress() float64 {
    time_year_start := time.Date(time.Now().Year(), 1, 1, 0, 0, 0, 0, time.UTC) //Time progressed in Hours
    time_year_end := time.Date(time.Now().Year() + 1 , 1, 1, 0, 0, 0, 0, time.UTC) // Get total time in hours
    return (float64(time.Since(time_year_start).Round(time.Second))/ float64(time_year_end.Sub(time_year_start).Round(time.Second))) * 100 // Year Progress
}

// Used to create a simple progress bar using keyboard characters
// Example: [-----||-----]
func getBitBar() string {
    myString := "["
    var value string
    for i := 1; i <= 25; i++ {
        if i == int(temp_answer)/ 4 {
            value = "||"
        } else {
            value = "-"
        }
        myString += value
    }
    myString += "]"
    return myString
}

func main() {
    // Start log...
    fmt.Println("Time Progress App")
    // Create a channel
    c := make(chan float64)
    // Start a routine in parallel with following statements
    go Updated(c)
    // Get the result from the routine
    for {
        answer := <- c
        // Tweet the Result using Twitter API
        fmt.Println(tweet(fmt.Sprintf("The Year has progressed:\n %s %d", getBitBar(), int(answer)) + "%"))
    }
}
Enter fullscreen mode Exit fullscreen mode

This is finally deployed on Heroku and is currently live at TimeProgressApp. So make sure to follow 😃 to receive time progress %.


In the end, this was a little experiment of mine and if you felt this was Insightful, do follow me on Twitter at TnvMadhav and let me know 😄

💖 💪 🙅 🚩
tnvmadhav
TnvMadhav⚡

Posted on November 14, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related