Skip to content

Commit

Permalink
Big Bang
Browse files Browse the repository at this point in the history
feat: main implementation
refactor: divided in packages
refactor: output produces string to print instead of printing directly
refactor: extracted constants for report config
refactor: upgraded go version to use slices std lib for contains
tests: tests for output package
tests: tests for logic package
docs: added README.md
  • Loading branch information
crossbone-magister committed Dec 3, 2024
1 parent b9c9913 commit be36ead
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 0 deletions.
54 changes: 54 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,go
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,go

### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work

### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets

# Local History for Visual Studio Code
.history/

# Built Visual Studio Code Extensions
*.vsix

### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide

# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,go

tag-percentage

coverage.out

dist/

.idea/
35 changes: 35 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
binary:
tag-percentage
goos:
#Timewarrior is officially distributed only on linux platforms
- linux
goarch:
- amd64

archives:
- id: binary
format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-snapshot"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Tag percentage Timewarrior extension

This [Timewarrior extension](https://timewarrior.net/docs/extensions/) calculates the percentage of time registered for a particular tag on the total for each day in the input intervals.

## Installation

1. Download the latest executable for your operating system from
the [releases page](https://github.com/crossbone-magister/tag-percentage/releases).
2. Add it to the Timewarrior extension folder as described in the [documentation](https://timewarrior.net/docs/api/).
3. Verify that the extension is active and installed by running `timew extensions`.

## Configuration

Add the entry `reports.tagpercentage.target` Timewarrior configuration to allow this extension to work.
Its value represents the tag to look for when calculating percentages.

For example:
```shell
timew config reports.tagpercentage.target project1
```

Sets the label `project1` as the target for this extension.


## Usage

In a terminal window, run `timew tag-percentage`. An example output could be:

```bash
2024-01-01 - project1: 80.39%
2024-01-02 - project1: 71.12%
2024-01-03 - project1: 23.67%
2024-01-04 - project1: 45.96%
2024-01-05 - project1: 85.71%
2024-01-06 - project1: 32.56%
Average percentage: 56.57%
```
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module tag-percentage

go 1.22.0

toolchain go1.23.3

require github.com/crossbone-magister/timewlib v0.3.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/crossbone-magister/timewlib v0.3.0 h1:fwWZUH4At14aEl9+yHriZs9xdHTsYrhDIdDuF8LpDLc=
github.com/crossbone-magister/timewlib v0.3.0/go.mod h1:g+5jGSrqG6+Ws7eTFx8GZ7xKFGuMh7kWC2xDU2bqk3c=
33 changes: 33 additions & 0 deletions logic/logic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package logic

import (
"fmt"
"github.com/crossbone-magister/timewlib"
"slices"
"time"
)

func CalculateTagPercentage(intervals []timewlib.Interval, targetTag string) (map[string]float64, float64) {
totalPerDay := make(map[string]time.Duration)
totalTargetTagPerDay := make(map[string]time.Duration)
for _, interval := range intervals {
y, m, d := interval.StartDate()
key := fmt.Sprintf("%d-%02d-%02d", y, m, d)
if _, ok := totalPerDay[key]; !ok {
totalPerDay[key] = 0
}
totalPerDay[key] += interval.Duration()
if slices.Contains(interval.Tags, targetTag) {
totalTargetTagPerDay[key] += interval.Duration()
}
}
var percentages = make(map[string]float64)
var average = 0.0
for day, total := range totalPerDay {
percentage := (totalTargetTagPerDay[day].Seconds() / total.Seconds()) * 100
percentages[day] = percentage
average += percentage
}
average /= float64(len(totalPerDay))
return percentages, average
}
30 changes: 30 additions & 0 deletions logic/logic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package logic

import (
"github.com/crossbone-magister/timewlib"
"testing"
"time"
)

func TestCalculateTagPercentage(t *testing.T) {
var interval1 = timewlib.NewInterval(10, 0, 11, 0)
interval1.Tags = []string{"target"}
var interval2 = timewlib.NewInterval(11, 0, 12, 0)
percentagesPerDay, averagePercentage := CalculateTagPercentage([]timewlib.Interval{
*interval1,
*interval2,
}, "target")
var label = time.Now().Format("2006-01-02")
if len(percentagesPerDay) <= 0 {
t.Errorf("PercentagesPerDay is empty")
}
if _, ok := percentagesPerDay[label]; !ok {
t.Errorf("PercentagesPerDay doesn't contain day %s", label)
}
if percentagesPerDay[label] != 50.0 {
t.Errorf("PercentagesPerDay for day %s is not 0.5 but %f", label, percentagesPerDay[label])
}
if averagePercentage != 50.0 {
t.Errorf("AveragePercentage is less than 0.5")
}
}
45 changes: 45 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
"fmt"
"github.com/crossbone-magister/timewlib"
"os"
"tag-percentage/logic"
"tag-percentage/output"
)

const TargetTagConfig = "reports.tagpercentage.target"

func main() {
parsed, err := timewlib.Parse(os.Stdin)
if err == nil {
var config timewlib.Configuration = parsed.Configuration
var targetTag = config[TargetTagConfig]
if targetTag != "" {
intervals, err := timewlib.Process(parsed.Intervals)
if err == nil {
if len(intervals) > 0 {
percentages, average := logic.CalculateTagPercentage(intervals, targetTag)
for _, row := range output.FormatPercentages(percentages, targetTag, average) {
fmt.Println(row)
}
} else {
fmt.Println(timewlib.GenerateNoDataMessage(config))
}
} else {
printErrorAndExit(err)
}
} else {
fmt.Println("No target tag specified")
os.Exit(1)
}
} else {
printErrorAndExit(err)
}

}

func printErrorAndExit(err error) {
fmt.Printf("Error while reading timewarrior input: %s\n", err)
os.Exit(1)
}
20 changes: 20 additions & 0 deletions output/output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package output

import (
"fmt"
"sort"
)

func FormatPercentages(percentages map[string]float64, targetTag string, average float64) []string {
var output = make([]string, 0)
var sortedDates = make([]string, 0, len(percentages))
for key, _ := range percentages {
sortedDates = append(sortedDates, key)
}
sort.Strings(sortedDates)
for _, date := range sortedDates {
output = append(output, fmt.Sprintf("%s - %s: %.2f%%", date, targetTag, percentages[date]))
}
output = append(output, fmt.Sprintf("Average percentage: %.2f%%", average))
return output
}
20 changes: 20 additions & 0 deletions output/output_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package output

import "testing"

func TestFormatPercentages(t *testing.T) {
var percentages = map[string]float64{
"02-12-2024": 36.74589,
}

formatted := FormatPercentages(percentages, "target", 47.231458)
expectedLineFormat := "02-12-2024 - target: 36.75%"
if formatted[0] != expectedLineFormat {
t.Errorf("Incorrected formatting for single row. Expected '%s', got '%s'", expectedLineFormat, formatted[0])
}
expectedAverageFormat := "Average percentage: 47.23%"
if formatted[1] != expectedAverageFormat {
t.Errorf("Incorrected formatting for average row. Expected '%s', got '%s'", expectedLineFormat, formatted[0])
}

}

0 comments on commit be36ead

Please sign in to comment.