"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:
— TimeProgressApp (@TimeProgressApp) November 10, 2020
[--------------------||----] 86%
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:
- Get the progress %
-
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
}
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 %
}
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
}
}
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)
}
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)
}
}
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
- Make sure that the calling function for
IsProgressed
doesn't just exit after getting the number. - 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
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)
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)
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()
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)
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)) + "%"))
}
}
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 😄
Top comments (2)
Nice! Curious what you've used for the Twitter integration, or did you build your own?
Thanks, no I did not build my own twitter implementation. I used an unofficial package that I found off Github