diff --git a/klog/app/cli/lib/helper.go b/klog/app/cli/lib/helper.go index cbeb084..86f43b6 100644 --- a/klog/app/cli/lib/helper.go +++ b/klog/app/cli/lib/helper.go @@ -22,7 +22,7 @@ func Reconcile(ctx app.Context, opts ReconcileOpts, creators []reconciling.Creat if err != nil { return err } - ctx.Print("\n" + parser.SerialiseRecords(ctx.Serialiser(), result.Record) + "\n") + ctx.Print("\n" + parser.SerialiseRecords(ctx.Serialiser(), result.Record).ToString() + "\n") opts.WarnArgs.PrintWarnings(ctx, ToRecords(result.AllRecords)) return nil } diff --git a/klog/app/cli/print.go b/klog/app/cli/print.go index 928d9a3..925e12d 100644 --- a/klog/app/cli/print.go +++ b/klog/app/cli/print.go @@ -1,12 +1,16 @@ package cli import ( + "github.com/jotaen/klog/klog" "github.com/jotaen/klog/klog/app" "github.com/jotaen/klog/klog/app/cli/lib" "github.com/jotaen/klog/klog/parser" + "github.com/jotaen/klog/klog/service" + "strings" ) type Print struct { + WithTotals bool `name:"with-totals" help:"Amend output with evaluated total times"` lib.FilterArgs lib.SortArgs lib.WarnArgs @@ -30,8 +34,74 @@ func (opt *Print) Run(ctx app.Context) error { return nil } records = opt.ApplySort(records) - ctx.Print("\n" + parser.SerialiseRecords(ctx.Serialiser(), records...) + "\n") + serialisedRecords := parser.SerialiseRecords(ctx.Serialiser(), records...) + output := func() string { + if opt.WithTotals { + return printWithDurations(ctx.Serialiser(), serialisedRecords) + } + return "\n" + serialisedRecords.ToString() + }() + ctx.Print(output + "\n") opt.WarnArgs.PrintWarnings(ctx, records) return nil } + +func printWithDurations(serialiser parser.Serialiser, ls parser.Lines) string { + type Prefix struct { + d klog.Duration + column int + } + var prefixes []*Prefix + maxColumnLengths := []int{0, 0} + var previousRecord klog.Record + previousEntry := -1 + for _, l := range ls { + prefix := func() *Prefix { + if l.Record == nil { + previousRecord = nil + previousEntry = -1 + return nil + } + if previousRecord == nil { + previousRecord = l.Record + return &Prefix{service.Total(l.Record), 0} + } + if l.EntryI != -1 && l.EntryI != previousEntry { + previousEntry = l.EntryI + return &Prefix{l.Record.Entries()[l.EntryI].Duration(), 1} + } else { + return nil + } + }() + prefixes = append(prefixes, prefix) + if prefix != nil && len(prefix.d.ToString()) > maxColumnLengths[prefix.column] { + maxColumnLengths[prefix.column] = len(prefix.d.ToString()) + } + } + RECORD_SEPARATOR := strings.Repeat("-", maxColumnLengths[0]) + "-+-" + strings.Repeat("-", maxColumnLengths[1]) + result := RECORD_SEPARATOR + "-+ " + "\n" + for i, l := range ls { + prefixText := "" + p := prefixes[i] + if l.Record == nil { + prefixText = RECORD_SEPARATOR + prefixText += "-+ " + } else { + column := []string{strings.Repeat(" ", maxColumnLengths[0]), strings.Repeat(" ", maxColumnLengths[1])} + if p != nil { + column[p.column] = strings.Repeat(" ", maxColumnLengths[0]-len(p.d.ToString())) + column[p.column] += serialiser.Duration(p.d) + } + prefixText += column[0] + prefixText += " | " + prefixText += column[1] + prefixText += " | " + } + result += prefixText + result += l.Text + result += "\n" + } + result += RECORD_SEPARATOR + "-+ " + return result +} diff --git a/klog/app/cli/print_test.go b/klog/app/cli/print_test.go index 0c2d0e6..ee5ac16 100644 --- a/klog/app/cli/print_test.go +++ b/klog/app/cli/print_test.go @@ -8,9 +8,18 @@ import ( ) func TestPrintOutEmptyInput(t *testing.T) { - state, err := NewTestingContext()._SetRecords(``)._Run((&Print{}).Run) - require.Nil(t, err) - assert.Equal(t, "", state.printBuffer) + { + state, err := NewTestingContext()._SetRecords(``)._Run((&Print{}).Run) + require.Nil(t, err) + assert.Equal(t, "", state.printBuffer) + } + { + state, err := NewTestingContext()._SetRecords(``)._Run((&Print{ + WithTotals: true, + }).Run) + require.Nil(t, err) + assert.Equal(t, "", state.printBuffer) + } } func TestPrintOutRecord(t *testing.T) { @@ -69,3 +78,41 @@ func TestPrintOutRecordsInChronologicalOrder(t *testing.T) { _Run((&Print{SortArgs: lib.SortArgs{Sort: "desc"}}).Run) assert.Equal(t, "\n2018-02-01\n\n2018-01-31\n\n2018-01-30\n\n", stateSortedDesc.printBuffer) } + +func TestPrintRecordsWithDurations(t *testing.T) { + state, err := NewTestingContext()._SetNow(2018, 02, 07, 19, 00)._SetRecords(` +2018-01-31 +Hello #world + 1h + +2018-02-04 + 15:00 - 17:22 + -1h1m + +2018-02-07 + 35m + Foo + 18:00 - ? I just + started something +`)._Run((&Print{ + WithTotals: true, + }).Run) + require.Nil(t, err) + assert.Equal(t, ` +------+-------+ + 1h | | 2018-01-31 + | | Hello #world + | 1h | 1h +------+-------+ +1h21m | | 2018-02-04 + | 2h22m | 15:00 - 17:22 + | -1h1m | -1h1m +------+-------+ + 35m | | 2018-02-07 + | 35m | 35m + | | Foo + | 0m | 18:00 - ? I just + | | started something +------+-------+ +`, state.printBuffer) +} diff --git a/klog/app/cli/report.go b/klog/app/cli/report.go index 7a2f006..e930b7d 100644 --- a/klog/app/cli/report.go +++ b/klog/app/cli/report.go @@ -95,7 +95,6 @@ func (opt *Report) Run(ctx app.Context) error { if opt.Diff { table.Fill("=").Fill("=") } - ctx.Print("\n") grandTotal := service.Total(records...) // Footer diff --git a/klog/parser/integration_test.go b/klog/parser/integration_test.go index 3e533b3..feef204 100644 --- a/klog/parser/integration_test.go +++ b/klog/parser/integration_test.go @@ -22,6 +22,6 @@ lines and contains a #tag as well. 7:00 - ? ` rs, _ := Parse(text) - s := SerialiseRecords(plainSerialiser{}, rs[0]) + s := SerialiseRecords(plainSerialiser{}, rs[0]).ToString() assert.Equal(t, text, s) } diff --git a/klog/parser/serialiser.go b/klog/parser/serialiser.go index b2fd4d6..4c431cf 100644 --- a/klog/parser/serialiser.go +++ b/klog/parser/serialiser.go @@ -17,46 +17,62 @@ type Serialiser interface { Time(klog.Time) string } +type Line struct { + Text string + Record klog.Record + EntryI int +} + +type Lines []Line + +func (ls Lines) ToString() string { + result := "" + for _, l := range ls { + result += l.Text + canonicalStyle.LineEnding.Get() + } + return result +} + // SerialiseRecords serialises records into the canonical string representation. // (So it doesn’t and cannot restore the original formatting!) -func SerialiseRecords(s Serialiser, rs ...klog.Record) string { - var text []string - for _, r := range rs { - text = append(text, serialiseRecord(s, r)) +func SerialiseRecords(s Serialiser, rs ...klog.Record) Lines { + var lines []Line + for i, r := range rs { + lines = append(lines, serialiseRecord(s, r)...) + if i < len(rs)-1 { + lines = append(lines, Line{"", nil, -1}) + } } - return strings.Join(text, "\n") + return lines } -var canonicalStyle = DefaultStyle() - -func serialiseRecord(s Serialiser, r klog.Record) string { - text := "" - text += s.Date(r.Date()) +func serialiseRecord(s Serialiser, r klog.Record) []Line { + var lines []Line + headline := s.Date(r.Date()) if r.ShouldTotal().InMinutes() != 0 { - text += " (" + s.ShouldTotal(r.ShouldTotal()) + ")" + headline += " (" + s.ShouldTotal(r.ShouldTotal()) + ")" } - text += canonicalStyle.LineEnding.Get() + lines = append(lines, Line{headline, r, -1}) if r.Summary() != nil { - text += s.Summary(SummaryText(r.Summary())) + canonicalStyle.LineEnding.Get() + lines = append(lines, Line{s.Summary(SummaryText(r.Summary())), r, -1}) } - for _, e := range r.Entries() { - text += canonicalStyle.Indentation.Get() - text += klog.Unbox[string](&e, + for entryI, e := range r.Entries() { + entryValue := klog.Unbox[string](&e, func(r klog.Range) string { return s.Range(r) }, func(d klog.Duration) string { return s.Duration(d) }, func(o klog.OpenRange) string { return s.OpenRange(o) }, ) + lines = append(lines, Line{canonicalStyle.Indentation.Get() + entryValue, r, entryI}) for i, l := range e.Summary().Lines() { + summaryText := s.Summary([]string{l}) if i == 0 && l != "" { - text += " " // separator + lines[len(lines)-1].Text += " " + summaryText } else if i >= 1 { - text += canonicalStyle.LineEnding.Get() + canonicalStyle.Indentation.Get() + canonicalStyle.Indentation.Get() + lines = append(lines, Line{canonicalStyle.Indentation.Get() + canonicalStyle.Indentation.Get() + summaryText, r, entryI}) } - text += s.Summary([]string{l}) } - text += canonicalStyle.LineEnding.Get() } - return text + return lines } type SummaryText []string @@ -64,3 +80,5 @@ type SummaryText []string func (s SummaryText) ToString() string { return strings.Join(s, canonicalStyle.LineEnding.Get()) } + +var canonicalStyle = DefaultStyle() diff --git a/klog/parser/serialiser_test.go b/klog/parser/serialiser_test.go index 1e267bc..896f7ba 100644 --- a/klog/parser/serialiser_test.go +++ b/klog/parser/serialiser_test.go @@ -18,18 +18,18 @@ func (ps plainSerialiser) SignedDuration(x klog.Duration) string { return x.ToSt func (ps plainSerialiser) Time(x klog.Time) string { return x.ToString() } func TestSerialiseNoRecordsToEmptyString(t *testing.T) { - text := SerialiseRecords(plainSerialiser{}, []klog.Record{}...) + text := SerialiseRecords(plainSerialiser{}, []klog.Record{}...).ToString() assert.Equal(t, "", text) } func TestSerialiseEndsWithNewlineIfContainsContent(t *testing.T) { - text := SerialiseRecords(plainSerialiser{}, klog.NewRecord(klog.Ɀ_Date_(2020, 01, 15))) + text := SerialiseRecords(plainSerialiser{}, klog.NewRecord(klog.Ɀ_Date_(2020, 01, 15))).ToString() lastChar := []rune(text)[len(text)-1] assert.Equal(t, '\n', lastChar) } func TestSerialiseRecordWithMinimalRecord(t *testing.T) { - text := SerialiseRecords(plainSerialiser{}, klog.NewRecord(klog.Ɀ_Date_(2020, 01, 15))) + text := SerialiseRecords(plainSerialiser{}, klog.NewRecord(klog.Ɀ_Date_(2020, 01, 15))).ToString() assert.Equal(t, `2020-01-15 `, text) } @@ -45,7 +45,7 @@ func TestSerialiseRecordWithCompleteRecord(t *testing.T) { r.AddDuration(klog.NewDuration(-1, -51), nil) r.AddRange(klog.Ɀ_Range_(klog.Ɀ_TimeYesterday_(23, 23), klog.Ɀ_Time_(4, 3)), nil) r.AddRange(klog.Ɀ_Range_(klog.Ɀ_Time_(22, 0), klog.Ɀ_TimeTomorrow_(0, 1)), nil) - text := SerialiseRecords(plainSerialiser{}, r) + text := SerialiseRecords(plainSerialiser{}, r).ToString() assert.Equal(t, `2020-01-15 (7h30m!) This is a multiline summary @@ -67,7 +67,7 @@ func TestSerialiseMultipleRecords(t *testing.T) { text := SerialiseRecords(plainSerialiser{}, []klog.Record{ klog.NewRecord(klog.Ɀ_Date_(2020, 01, 15)), klog.NewRecord(klog.Ɀ_Date_(2020, 01, 20)), - }...) + }...).ToString() assert.Equal(t, `2020-01-15 2020-01-20