From eb05e6d5882636d3a52cea93a2657bb1c300d449 Mon Sep 17 00:00:00 2001 From: Riyyi Date: Mon, 8 Jul 2024 17:27:41 +0200 Subject: [PATCH] Initial commit --- .gitignore | 5 ++ go.mod | 7 +++ go.sum | 13 +++++ main.go | 45 +++++++++++++++ src/api.go | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/err.go | 13 +++++ src/file.go | 46 +++++++++++++++ 7 files changed, 292 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 src/api.go create mode 100644 src/err.go create mode 100644 src/file.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e51b71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Directories + +# Files + +worklog diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..970c228 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module worklog + +go 1.22.5 + +require github.com/alexflint/go-arg v1.5.1 + +require github.com/alexflint/go-scalar v1.2.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..43a8011 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= +github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= +github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= +github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4d0b2b0 --- /dev/null +++ b/main.go @@ -0,0 +1,45 @@ +// go mod init worklog +// go build +// go run . +// go mod tidy + +package main + +import "errors" +import "os" + +import "github.com/alexflint/go-arg" + +import "worklog/src" + +type Args struct { + Decl string `arg:"-d,--decl" help:"Generate travel declaration table" placeholder:"MONTH"` + Process bool `arg:"-p,--process" help:"Process specified file and call Jira API"` + File string `arg:"positional,required" help:"the worklog file to process"` +} + +func (Args) Description() string { + return "\nworklog - process a worklog file\n" +} + +func main() { + var args Args + parser := arg.MustParse(&args) + + _, err := os.Stat(args.File); + if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) { + parser.Fail("file was not readable: " + args.File) + } + + if args.Process { + var api src.Api = src.MakeApi() + var job = func(line string, line_number int) string { + return api.Process(line, line_number) + } + src.Parse(args.File, job) + } + + if args.Decl != "" { + // TODO: generate declaration table.. + } +} diff --git a/src/api.go b/src/api.go new file mode 100644 index 0000000..c5b631f --- /dev/null +++ b/src/api.go @@ -0,0 +1,163 @@ +package src + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +type Date struct { + clock_in string + location string + clock_out string + + last_time string + last_item_id string + last_description string +} + +type Api struct { + clock_in *regexp.Regexp + task_line *regexp.Regexp + clock_out *regexp.Regexp + dates map[string]Date +} + +// Constructor +func MakeApi() Api { + return Api{ + clock_in: compileRegex(`^\s*\| [0-9]{4}-[0-9]{2}-[0-9]{2} \|\s+IN\s+\|`), + task_line: compileRegex(`^\s*\| [0-9]{4}-[0-9]{2}-[0-9]{2} \| [0-9]{2}:[0-9]{2} \|`), + clock_out: compileRegex(`^\s*\| [0-9]{4}-[0-9]{2}-[0-9]{2} \|\s+OUT\s+\|`), + dates: make(map[string]Date), + } +} + +func (self *Api) Process(line string, line_number int) string { + var err error + if self.clock_in.MatchString(line) { + err = self.parseClockIn(line, line_number) + } else if self.task_line.MatchString(line) { + err = self.parseTask(line, line_number) + } else if self.clock_out.MatchString(line) { + err = self.parseClockOut(line, line_number) + } + assert(err) + + // fmt.Println(line) + return line +} + +// ----------------------------------------- + +func compileRegex(pattern string) *regexp.Regexp { + regex, err := regexp.Compile(pattern) + assert(err) + + return regex +} + +func (self *Api) parseLine(line string, line_number int, size int) ([]string, error) { + var data []string = strings.Split(line, "|") + + if len(data) != size + 2 { + return nil, errors.New("malformed line " + strconv.Itoa(line_number) + "\n" + line) + } + + data = data[1:size + 1] + for i, value := range data { + data[i] = strings.TrimSpace(value) + + if len(data[i]) == 0 { + return nil, errors.New("malformed line " + strconv.Itoa(line_number) + "\n" + line) + } + } + + return data, nil +} + +func (self *Api) parseClockIn(line string, line_number int) error { + data, err := self.parseLine(line, line_number, 4) + if err != nil { return err } + + // Set clock_in, location + var date Date = self.dates[data[0]] + date.clock_in = data[2] + date.location = data[3] + self.dates[data[0]] = date + + return nil +} + +func (self *Api) parseTask(line string, line_number int) error { + data, err := self.parseLine(line, line_number, 5) + if err != nil { return err } + + var date Date = self.dates[data[0]] + + if date.clock_in == "" { + return errors.New("no clock-in time found") + } + + // Call API for the previous task + if date.last_time != "" && date.last_item_id != "" && date.last_description != "" { + err = self.callApi(data[0], date.last_time, data[1], date.last_item_id, date.last_description) + } + + if err != nil { return err } + + // Set last_time, last_item_id, description + if data[3] == "X" { + date.last_time = data[1] + date.last_item_id = data[2] + date.last_description = data[4] + } else { // "V", task is already processed + date.last_time = "" + date.last_item_id = "" + date.last_description = "" + } + self.dates[data[0]] = date + + return nil +} + +func (self *Api) parseClockOut(line string, line_number int) error { + data, err := self.parseLine(line, line_number, 3) + if err != nil { return err } + + // Set clock_out + var date Date = self.dates[data[0]] + date.clock_out = data[2] + self.dates[data[0]] = date + + if date.last_time == "" || date.last_item_id == "" || date.last_description == "" { + return errors.New("no previous task to use clock-out on") + } + + // Call API for last task of the day + self.callApi(data[0], date.last_time, date.clock_out, date.last_item_id, date.last_description) + + return nil +} + +func (self *Api) callApi(date string, from_time string, to_time string, item_id string, description string) error { + fmt.Println("API |" + date + "|" + from_time + "|" + to_time + "|" + item_id + "|" + description) + + // parse line + // call API + // error checking + + return nil +} + +// Example worklog: + +// | 2024-07-06 | IN | 08:30 | Office | + +// | 2024-07-06 | 09:00 | T1-123 | V | I did nothing! | +// | 2024-07-06 | 09:30 | T1-456 | X | Blabla | +// | 2024-07-06 | 11:00 | T1-789 | X | - | + +// | 2024-07-06 | OUT | 13:00 | diff --git a/src/err.go b/src/err.go new file mode 100644 index 0000000..6439c17 --- /dev/null +++ b/src/err.go @@ -0,0 +1,13 @@ +package src + +func assert(err error) { + if err != nil { + panic(err) + } +} + +func verify(condition bool, message string) { + if !condition { + panic(message) + } +} diff --git a/src/file.go b/src/file.go new file mode 100644 index 0000000..e4cd194 --- /dev/null +++ b/src/file.go @@ -0,0 +1,46 @@ +package src + +import "bufio" +import "os" + +func Parse(path string, job func(line string, line_number int) string) { + // Input file + file, err := os.Open(path) + assert(err) + defer file.Close() + var scanner *bufio.Scanner = bufio.NewScanner(file) + + // Output file + output_file, err := os.Create(path + ".tmp") + assert(err) + defer output_file.Close() + var writer *bufio.Writer = bufio.NewWriter(output_file) + defer writer.Flush() + + var line string + var line_number int = 1 + for scanner.Scan() { + line = scanner.Text() + line = job(line, line_number) + line_number++ + + // Write line to output_file + _, err := writer.WriteString(line + "\n") + assert(err) + } + + // Detect table if it was at the end of the file + job("", line_number) + + err = scanner.Err() + assert(err) + + // move file +} + +// - [v] while looping, start writing a new file, .tmp, Q: write per line or per chunk? how big are the chunks? +// - [v] mark table start with processed mark +// - [ ] on table end, call into REST API +// - [ ] if true, continue +// - [ ] if false, delete .tmp file, panic +// - [ ] if no errors, overwrite file with .tmp file