From be36ead3b206978ba1a2db58b4b913cbdfe59287 Mon Sep 17 00:00:00 2001 From: Ivo Forlin Date: Tue, 3 Dec 2024 18:14:47 +0100 Subject: [PATCH] Big Bang 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 --- .gitignore | 54 +++++++++++++++++++++++++++++++++++++++++++ .goreleaser.yml | 35 ++++++++++++++++++++++++++++ README.md | 37 +++++++++++++++++++++++++++++ go.mod | 7 ++++++ go.sum | 2 ++ logic/logic.go | 33 ++++++++++++++++++++++++++ logic/logic_test.go | 30 ++++++++++++++++++++++++ main.go | 45 ++++++++++++++++++++++++++++++++++++ output/output.go | 20 ++++++++++++++++ output/output_test.go | 20 ++++++++++++++++ 10 files changed, 283 insertions(+) create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logic/logic.go create mode 100644 logic/logic_test.go create mode 100644 main.go create mode 100644 output/output.go create mode 100644 output/output_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54f7cf0 --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..3c909c1 --- /dev/null +++ b/.goreleaser.yml @@ -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:' \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..63c5b0d --- /dev/null +++ b/README.md @@ -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% +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4287ff9 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module tag-percentage + +go 1.22.0 + +toolchain go1.23.3 + +require github.com/crossbone-magister/timewlib v0.3.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2a2d1e3 --- /dev/null +++ b/go.sum @@ -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= diff --git a/logic/logic.go b/logic/logic.go new file mode 100644 index 0000000..42c6ea8 --- /dev/null +++ b/logic/logic.go @@ -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 +} diff --git a/logic/logic_test.go b/logic/logic_test.go new file mode 100644 index 0000000..4850114 --- /dev/null +++ b/logic/logic_test.go @@ -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") + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..b92fab7 --- /dev/null +++ b/main.go @@ -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) +} diff --git a/output/output.go b/output/output.go new file mode 100644 index 0000000..516d957 --- /dev/null +++ b/output/output.go @@ -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 +} diff --git a/output/output_test.go b/output/output_test.go new file mode 100644 index 0000000..ff0538a --- /dev/null +++ b/output/output_test.go @@ -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]) + } + +}