diff --git a/klog/app/cli/args/filter.go b/klog/app/cli/args/filter.go index d12931a..c0d855c 100644 --- a/klog/app/cli/args/filter.go +++ b/klog/app/cli/args/filter.go @@ -36,6 +36,8 @@ type FilterArgs struct { // General filters: Tags []klog.Tag `name:"tag" placeholder:"TAG" group:"Filter Flags:" help:"Entries that match these tags (either in the record summary or the entry summary). You can omit the leading '#'."` Filter string `name:"filter" placeholder:"EXPR" group:"Filter Flags:" help:"Entries that match this filter expression. Run 'klog info --filtering' to learn how expressions works."` + + hasPartialRecordsWithShouldTotal bool // Field only for internal use } func (args *FilterArgs) ApplyFilter(now gotime.Time, rs []klog.Record) ([]klog.Record, app.Error) { @@ -125,7 +127,10 @@ func (args *FilterArgs) ApplyFilter(now gotime.Time, rs []klog.Record) ([]klog.R // Apply filters, if applicable: if len(predicates) > 0 { - rs = filter.Filter(filter.And{Predicates: predicates}, rs) + hprws := false + rs, hprws = filter.Filter(filter.And{Predicates: predicates}, rs) + args.hasPartialRecordsWithShouldTotal = hprws + } return rs, nil } diff --git a/klog/app/cli/args/misc.go b/klog/app/cli/args/misc.go index f7930cb..f6b232c 100644 --- a/klog/app/cli/args/misc.go +++ b/klog/app/cli/args/misc.go @@ -13,6 +13,16 @@ type DiffArgs struct { Diff bool `name:"diff" short:"d" help:"Show difference between actual and should-total time."` } +// GetWarning returns a warning if the user applied entry-level filtering (partial +// records) *and* requested to compute the should-total diff, as that may yield +// nonsensical results. +func (args *DiffArgs) GetWarning(filterArgs FilterArgs) service.UsageWarning { + if args.Diff && filterArgs.hasPartialRecordsWithShouldTotal { + return service.DiffEntryFilteringWarning + } + return service.UsageWarning{} +} + type NoStyleArgs struct { NoStyle bool `name:"no-style" help:"Do not style or colour the values."` } diff --git a/klog/app/cli/args/now.go b/klog/app/cli/args/now.go index 8318a59..251280b 100644 --- a/klog/app/cli/args/now.go +++ b/klog/app/cli/args/now.go @@ -34,9 +34,11 @@ func (args *NowArgs) HadOpenRange() bool { return args.hadOpenRange } -func (args *NowArgs) GetNowWarnings() []string { +// GetWarning warns the user that they specified the --now flag but there actually +// weren’t any closable ranges in the data. +func (args *NowArgs) GetWarning() service.UsageWarning { if args.Now && !args.hadOpenRange { - return []string{"You specified --now, but there was no open-ended time range"} + return service.PointlessNowWarning } - return nil + return service.UsageWarning{} } diff --git a/klog/app/cli/args/warn.go b/klog/app/cli/args/warn.go index 2249ecd..a557a5c 100644 --- a/klog/app/cli/args/warn.go +++ b/klog/app/cli/args/warn.go @@ -11,7 +11,7 @@ type WarnArgs struct { NoWarn bool `name:"no-warn" help:"Suppress warnings about potential mistakes or logical errors."` } -func (args *WarnArgs) PrintWarnings(ctx app.Context, records []klog.Record, additionalWarnings []string) { +func (args *WarnArgs) PrintWarnings(ctx app.Context, records []klog.Record, additionalWarnings []service.UsageWarning) { styler, _ := ctx.Serialise() warnings := args.GatherWarnings(ctx, records, additionalWarnings) for _, w := range warnings { @@ -19,11 +19,16 @@ func (args *WarnArgs) PrintWarnings(ctx app.Context, records []klog.Record, addi } } -func (args *WarnArgs) GatherWarnings(ctx app.Context, records []klog.Record, additionalWarnings []string) []string { +func (args *WarnArgs) GatherWarnings(ctx app.Context, records []klog.Record, additionalWarnings []service.UsageWarning) []string { if args.NoWarn { return nil } disabledCheckers := ctx.Config().NoWarnings.UnwrapOr(service.NewDisabledCheckers()) - dataWarnings := service.CheckForWarnings(ctx.Now(), records, disabledCheckers) - return append(dataWarnings, additionalWarnings...) + warnings := service.CheckForWarnings(ctx.Now(), records, disabledCheckers) + for _, warn := range additionalWarnings { + if warn != (service.UsageWarning{}) && !disabledCheckers[warn.Name] { + warnings = append(warnings, warn.Message) + } + } + return warnings } diff --git a/klog/app/cli/json.go b/klog/app/cli/json.go index 5e7c737..734a6bb 100644 --- a/klog/app/cli/json.go +++ b/klog/app/cli/json.go @@ -4,6 +4,7 @@ import ( "github.com/jotaen/klog/klog/app" "github.com/jotaen/klog/klog/app/cli/args" "github.com/jotaen/klog/klog/parser/json" + "github.com/jotaen/klog/klog/service" ) type Json struct { @@ -50,7 +51,7 @@ func (opt *Json) Run(ctx app.Context) app.Error { return fErr } records = opt.ApplySort(records) - warnings := opt.GatherWarnings(ctx, records, opt.GetNowWarnings()) + warnings := opt.GatherWarnings(ctx, records, []service.UsageWarning{opt.GetWarning()}) ctx.Print(json.ToJson(records, nil, warnings, opt.Pretty) + "\n") return nil } diff --git a/klog/app/cli/print.go b/klog/app/cli/print.go index 7bf8972..82216d7 100644 --- a/klog/app/cli/print.go +++ b/klog/app/cli/print.go @@ -23,9 +23,10 @@ type Print struct { func (opt *Print) Help() string { return ` Outputs data on the terminal, by default with syntax-highlighting turned on. -Note that the output doesn’t resemble the file byte by byte, but the command may apply some minor clean-ups of the formatting. +Note that the output doesn’t resemble the file verbatim, but it may apply some minor formatting. + +If run with filter flags, it only outputs those entries that match the filter clauses. E.g., when filtering for a tag that only appears particular entries, it will exclude all other entries from that record. -If run with filter flags, it only outputs those entries that match the filter clauses. You can optionally also sort the records, or print out the total times for each record and entry. ` } diff --git a/klog/app/cli/report.go b/klog/app/cli/report.go index 4c426bd..68edc51 100644 --- a/klog/app/cli/report.go +++ b/klog/app/cli/report.go @@ -145,7 +145,7 @@ func (opt *Report) Run(ctx app.Context) app.Error { } table.Collect(ctx.Print) - opt.WarnArgs.PrintWarnings(ctx, records, opt.GetNowWarnings()) + opt.WarnArgs.PrintWarnings(ctx, records, []service.UsageWarning{opt.NowArgs.GetWarning(), opt.DiffArgs.GetWarning(opt.FilterArgs)}) return nil } diff --git a/klog/app/cli/tags.go b/klog/app/cli/tags.go index 37c46b7..7dbe88c 100644 --- a/klog/app/cli/tags.go +++ b/klog/app/cli/tags.go @@ -94,6 +94,6 @@ func (opt *Tags) Run(ctx app.Context) app.Error { } } table.Collect(ctx.Print) - opt.WarnArgs.PrintWarnings(ctx, records, opt.GetNowWarnings()) + opt.WarnArgs.PrintWarnings(ctx, records, []service.UsageWarning{opt.NowArgs.GetWarning()}) return nil } diff --git a/klog/app/cli/today.go b/klog/app/cli/today.go index 0aae8fd..57862b5 100644 --- a/klog/app/cli/today.go +++ b/klog/app/cli/today.go @@ -191,7 +191,7 @@ func handle(opt *Today, ctx app.Context) app.Error { } } table.Collect(ctx.Print) - opt.WarnArgs.PrintWarnings(ctx, records, opt.GetNowWarnings()) + opt.WarnArgs.PrintWarnings(ctx, records, []service.UsageWarning{opt.NowArgs.GetWarning()}) return nil } diff --git a/klog/app/cli/total.go b/klog/app/cli/total.go index e5e685d..0be2c77 100644 --- a/klog/app/cli/total.go +++ b/klog/app/cli/total.go @@ -59,6 +59,6 @@ func (opt *Total) Run(ctx app.Context) app.Error { return "s" }())) - opt.WarnArgs.PrintWarnings(ctx, records, opt.GetNowWarnings()) + opt.WarnArgs.PrintWarnings(ctx, records, []service.UsageWarning{opt.NowArgs.GetWarning(), opt.DiffArgs.GetWarning(opt.FilterArgs)}) return nil } diff --git a/klog/app/config.go b/klog/app/config.go index 0e61889..649e3e4 100644 --- a/klog/app/config.go +++ b/klog/app/config.go @@ -239,7 +239,14 @@ var CONFIG_FILE_ENTRIES = []ConfigFileEntries[any]{ Name: "no_warnings", Help: Help{ Summary: "Whether klog should suppress certain warnings when processing files.", - Value: "The config property must be one (or several comma-separated) of: `UNCLOSED_OPEN_RANGE` (for unclosed open ranges in past records), `FUTURE_ENTRIES` (for records/entries in the future), `OVERLAPPING_RANGES` (for time ranges that overlap), `MORE_THAN_24H` (if there is a record with more than 24h total). Multiple values must be separated by a comma, e.g.: `UNCLOSED_OPEN_RANGE, MORE_THAN_24H`.", + Value: "The config property must be one or several (comma-separated) of: " + + "`UNCLOSED_OPEN_RANGE` (for unclosed open ranges in past records), " + + "`FUTURE_ENTRIES` (for records/entries in the future), " + + "`OVERLAPPING_RANGES` (for time ranges that overlap), " + + "`MORE_THAN_24H` (if there is a record with more than 24h total), " + + "`POINTLESS_NOW` (when using --now without any open ranges), " + + "`ENTRY_FILTERED_DIFFING` (when combining --diff and entry-level filtering). " + + "Multiple values must be separated by a comma, e.g.: `UNCLOSED_OPEN_RANGE, MORE_THAN_24H`.", Default: "If absent/empty, klog prints all available warnings.", }, read: func(value string, config *Config) error { diff --git a/klog/service/filter/filter.go b/klog/service/filter/filter.go index a3ed286..29f68ce 100644 --- a/klog/service/filter/filter.go +++ b/klog/service/filter/filter.go @@ -4,20 +4,33 @@ import ( "github.com/jotaen/klog/klog" ) -func Filter(p Predicate, rs []klog.Record) []klog.Record { +// Filter goes through a list of records and only keeps those that match the +// given predicate. The records may be returned partially, keeping only those +// entries that match the predicate. +// The second return value indicates whether there are partial records with a +// should-total set, as this may yield nonsensical results in a subsequent evaluation. +func Filter(p Predicate, rs []klog.Record) ([]klog.Record, bool) { var res []klog.Record + hasPartialRecordsWithShouldTotal := false for _, r := range rs { - var es []klog.Entry - for i, e := range r.Entries() { - if p.Matches(queriedEntry{r, r.Entries()[i]}) { - es = append(es, e) + if len(r.Entries()) == 0 && p.MatchesEmptyRecord(r) { + res = append(res, r) + } else { + var es []klog.Entry + for i, e := range r.Entries() { + if p.Matches(r, r.Entries()[i]) { + es = append(es, e) + } } + if len(es) == 0 { + continue + } + if len(es) != len(r.Entries()) && r.ShouldTotal() != nil { + hasPartialRecordsWithShouldTotal = true + } + r.SetEntries(es) + res = append(res, r) } - if len(es) == 0 { - continue - } - r.SetEntries(es) - res = append(res, r) } - return res + return res, hasPartialRecordsWithShouldTotal } diff --git a/klog/service/filter/filter_test.go b/klog/service/filter/filter_test.go index 06f37d3..db0fc06 100644 --- a/klog/service/filter/filter_test.go +++ b/klog/service/filter/filter_test.go @@ -4,151 +4,239 @@ import ( "testing" "github.com/jotaen/klog/klog" - "github.com/jotaen/klog/klog/service" + "github.com/jotaen/klog/klog/parser" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func sampleRecordsForQuerying() []klog.Record { - return []klog.Record{ - func() klog.Record { - // Note that records without entries never match any query. - r := klog.NewRecord(klog.Ɀ_Date_(1999, 12, 30)) - r.SetSummary(klog.Ɀ_RecordSummary_("Hello World", "#foo")) - return r - }(), func() klog.Record { - r := klog.NewRecord(klog.Ɀ_Date_(1999, 12, 31)) - r.AddDuration(klog.NewDuration(5, 0), klog.Ɀ_EntrySummary_("#bar")) - return r - }(), func() klog.Record { - r := klog.NewRecord(klog.Ɀ_Date_(2000, 1, 1)) - r.SetSummary(klog.Ɀ_RecordSummary_("#foo")) - r.AddDuration(klog.NewDuration(0, 15), nil) - r.AddDuration(klog.NewDuration(6, 0), klog.Ɀ_EntrySummary_("#bar")) - r.AddDuration(klog.NewDuration(0, -30), nil) - return r - }(), func() klog.Record { - r := klog.NewRecord(klog.Ɀ_Date_(2000, 1, 2)) - r.SetSummary(klog.Ɀ_RecordSummary_("#foo")) - r.AddDuration(klog.NewDuration(7, 0), nil) - return r - }(), func() klog.Record { - r := klog.NewRecord(klog.Ɀ_Date_(2000, 1, 3)) - r.SetSummary(klog.Ɀ_RecordSummary_("#foo=a")) - r.AddDuration(klog.NewDuration(4, 0), klog.Ɀ_EntrySummary_("test", "foo #bar=1")) - r.AddDuration(klog.NewDuration(4, 0), klog.Ɀ_EntrySummary_("#bar=2")) - r.Start(klog.NewOpenRange(klog.Ɀ_Time_(12, 00)), nil) - return r - }(), + rs, _, err := parser.NewSerialParser().Parse(` +1999-12-29 +No tags here + +1999-12-30 +Hello World #foo #first + +1999-12-31 + 5h #bar [300] + +2000-01-01 +#foo #third + 1:30-1:45 [15] + 6h #bar [360] + -30m [-30] + +2000-01-02 +#foo #fourth + 7h #xyz [420] + +2000-01-03 +#foo=a #fifth + 12:00-16:00 [240] + #bar=1 + 3h #bar=2 [180] + 12:00-? [0] +`) + if err != nil { + panic(err) + } + return rs +} + +type expect struct { + date klog.Date + durations []int +} + +func assertResult(t *testing.T, es []expect, rs []klog.Record) { + require.Equal(t, len(es), len(rs), "unexpected number of records") + for i, expct := range es { + assert.Equal(t, expct.date, rs[i].Date(), "unexpected date") + require.Equal(t, len(expct.durations), len(rs[i].Entries()), "unexpected number of entries") + actualDurations := make([]int, len(rs[i].Entries())) + for j, e := range rs[i].Entries() { + actualDurations[j] = e.Duration().InMinutes() + } + assert.Equal(t, expct.durations, actualDurations, "unexpected duration") } } func TestQueryWithNoClauses(t *testing.T) { - rs := Filter(And{}, sampleRecordsForQuerying()) - require.Len(t, rs, 4) - assert.Equal(t, klog.NewDuration(5+6+7+8, -30+15), service.Total(rs...)) + rs, hprws := Filter(And{}, sampleRecordsForQuerying()) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 29), []int{}}, + {klog.Ɀ_Date_(1999, 12, 30), []int{}}, + {klog.Ɀ_Date_(1999, 12, 31), []int{300}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{15, 360, -30}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180, 0}}, + }, rs) } func TestQueryWithAtDate(t *testing.T) { - rs := Filter(IsInDateRange{ + rs, hprws := Filter(IsInDateRange{ From: klog.Ɀ_Date_(2000, 1, 2), To: klog.Ɀ_Date_(2000, 1, 2), }, sampleRecordsForQuerying()) - require.Len(t, rs, 1) - assert.Equal(t, klog.NewDuration(7, 0), service.Total(rs...)) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + }, rs) } func TestQueryWithAfter(t *testing.T) { - rs := Filter(IsInDateRange{ + rs, hprws := Filter(IsInDateRange{ From: klog.Ɀ_Date_(2000, 1, 1), To: nil, }, sampleRecordsForQuerying()) - require.Len(t, rs, 3) - assert.Equal(t, 1, rs[0].Date().Day()) - assert.Equal(t, 2, rs[1].Date().Day()) - assert.Equal(t, 3, rs[2].Date().Day()) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 1), []int{15, 360, -30}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180, 0}}, + }, rs) } func TestQueryWithBefore(t *testing.T) { - rs := Filter(IsInDateRange{ + rs, hprws := Filter(IsInDateRange{ From: nil, To: klog.Ɀ_Date_(2000, 1, 1), }, sampleRecordsForQuerying()) - require.Len(t, rs, 2) - assert.Equal(t, 31, rs[0].Date().Day()) - assert.Equal(t, 1, rs[1].Date().Day()) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 29), []int{}}, + {klog.Ɀ_Date_(1999, 12, 30), []int{}}, + {klog.Ɀ_Date_(1999, 12, 31), []int{300}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{15, 360, -30}}, + }, rs) } -func TestQueryWithTagOnEntries(t *testing.T) { - rs := Filter(HasTag{klog.NewTagOrPanic("bar", "")}, sampleRecordsForQuerying()) - require.Len(t, rs, 3) - assert.Equal(t, 31, rs[0].Date().Day()) - assert.Equal(t, 1, rs[1].Date().Day()) - assert.Equal(t, 3, rs[2].Date().Day()) - assert.Equal(t, klog.NewDuration(5+8+6, 0), service.Total(rs...)) +func TestQueryWithTagOnOverallSummary(t *testing.T) { + rs, hprws := Filter(HasTag{klog.NewTagOrPanic("foo", "")}, sampleRecordsForQuerying()) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 30), []int{}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{15, 360, -30}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180, 0}}, + }, rs) } -func TestQueryWithTagOnOverallSummary(t *testing.T) { - rs := Filter(HasTag{klog.NewTagOrPanic("foo", "")}, sampleRecordsForQuerying()) - require.Len(t, rs, 3) - assert.Equal(t, 1, rs[0].Date().Day()) - assert.Equal(t, 2, rs[1].Date().Day()) - assert.Equal(t, 3, rs[2].Date().Day()) - assert.Equal(t, klog.NewDuration(6+7+8, -30+15), service.Total(rs...)) +func TestQueryWithTagOnEntries(t *testing.T) { + rs, hprws := Filter(HasTag{klog.NewTagOrPanic("bar", "")}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 31), []int{300}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{360}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180}}, + }, rs) } func TestQueryWithTagOnEntriesAndInSummary(t *testing.T) { - rs := Filter(And{[]Predicate{HasTag{klog.NewTagOrPanic("foo", "")}, HasTag{klog.NewTagOrPanic("bar", "")}}}, sampleRecordsForQuerying()) - require.Len(t, rs, 2) - assert.Equal(t, 1, rs[0].Date().Day()) - assert.Equal(t, 3, rs[1].Date().Day()) - assert.Equal(t, klog.NewDuration(8+6, 0), service.Total(rs...)) + rs, hprws := Filter(And{[]Predicate{HasTag{klog.NewTagOrPanic("foo", "")}, HasTag{klog.NewTagOrPanic("bar", "")}}}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 1), []int{360}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180}}, + }, rs) } func TestQueryWithTagValues(t *testing.T) { - rs := Filter(HasTag{klog.NewTagOrPanic("foo", "a")}, sampleRecordsForQuerying()) - require.Len(t, rs, 1) - assert.Equal(t, 3, rs[0].Date().Day()) - assert.Equal(t, klog.NewDuration(8, 0), service.Total(rs...)) + rs, hprws := Filter(HasTag{klog.NewTagOrPanic("foo", "a")}, sampleRecordsForQuerying()) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180, 0}}, + }, rs) } func TestQueryWithTagValuesInEntries(t *testing.T) { - rs := Filter(HasTag{klog.NewTagOrPanic("bar", "1")}, sampleRecordsForQuerying()) - require.Len(t, rs, 1) - assert.Equal(t, 3, rs[0].Date().Day()) - assert.Equal(t, klog.NewDuration(4, 0), service.Total(rs...)) + rs, hprws := Filter(HasTag{klog.NewTagOrPanic("bar", "1")}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 3), []int{240}}, + }, rs) } func TestQueryWithTagNonMatchingValues(t *testing.T) { - rs := Filter(HasTag{klog.NewTagOrPanic("bar", "3")}, sampleRecordsForQuerying()) - require.Len(t, rs, 0) + rs, hprws := Filter(HasTag{klog.NewTagOrPanic("bar", "3")}, sampleRecordsForQuerying()) + assert.False(t, hprws) + assertResult(t, []expect{}, rs) } func TestQueryWithEntryTypes(t *testing.T) { { - rs := Filter(IsEntryType{ENTRY_TYPE_DURATION}, sampleRecordsForQuerying()) - require.Len(t, rs, 4) - assert.Equal(t, klog.NewDuration(0, 1545), service.Total(rs...)) + rs, hprws := Filter(IsEntryType{ENTRY_TYPE_DURATION}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 31), []int{300}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{360, -30}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{180}}, + }, rs) + } + { + rs, hprws := Filter(IsEntryType{ENTRY_TYPE_DURATION_NEGATIVE}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 1), []int{-30}}, + }, rs) + } + { + rs, hprws := Filter(IsEntryType{ENTRY_TYPE_DURATION_POSITIVE}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 31), []int{300}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{360}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{180}}, + }, rs) } { - rs := Filter(IsEntryType{ENTRY_TYPE_DURATION_NEGATIVE}, sampleRecordsForQuerying()) - require.Len(t, rs, 1) - assert.Equal(t, 1, rs[0].Date().Day()) - assert.Equal(t, klog.NewDuration(0, -30), service.Total(rs...)) + rs, hprws := Filter(IsEntryType{ENTRY_TYPE_RANGE}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 1), []int{15}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240}}, + }, rs) } { - rs := Filter(IsEntryType{ENTRY_TYPE_DURATION_POSITIVE}, sampleRecordsForQuerying()) - require.Len(t, rs, 4) - assert.Equal(t, klog.NewDuration(0, 1575), service.Total(rs...)) + rs, hprws := Filter(IsEntryType{ENTRY_TYPE_OPEN_RANGE}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 3), []int{0}}, + }, rs) } +} + +func TestComplexFilterQueries(t *testing.T) { { - rs := Filter(IsEntryType{ENTRY_TYPE_RANGE}, sampleRecordsForQuerying()) - require.Len(t, rs, 0) - assert.Equal(t, klog.NewDuration(0, 0), service.Total(rs...)) + rs, hprws := Filter(Or{[]Predicate{ + IsInDateRange{From: klog.Ɀ_Date_(2000, 1, 2), To: nil}, + HasTag{klog.NewTagOrPanic("first", "")}, + And{[]Predicate{ + Not{HasTag{klog.NewTagOrPanic("something", "1")}}, + IsEntryType{ENTRY_TYPE_RANGE}, + }}, + }}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 30), []int{}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{15}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180, 0}}, + }, rs) } { - rs := Filter(IsEntryType{ENTRY_TYPE_OPEN_RANGE}, sampleRecordsForQuerying()) - require.Len(t, rs, 1) - assert.Equal(t, klog.NewDuration(0, 0), service.Total(rs...)) + rs, hprws := Filter(And{[]Predicate{ + IsInDateRange{From: klog.Ɀ_Date_(2000, 1, 1), To: klog.Ɀ_Date_(2000, 1, 3)}, + HasTag{klog.NewTagOrPanic("bar", "")}, + Not{HasTag{klog.NewTagOrPanic("third", "")}}, + IsEntryType{ENTRY_TYPE_RANGE}, + }}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 3), []int{240}}, + }, rs) } } diff --git a/klog/service/filter/predicate.go b/klog/service/filter/predicate.go index 6c1778a..7b30b8f 100644 --- a/klog/service/filter/predicate.go +++ b/klog/service/filter/predicate.go @@ -7,32 +7,36 @@ import ( "github.com/jotaen/klog/klog" ) -type queriedEntry struct { - parent klog.Record - entry klog.Entry -} - +// Predicate is the generic base type for all predicates. type Predicate interface { - Matches(queriedEntry) bool + // Matches returns true if the record’s entry satisfies the predicate. + Matches(klog.Record, klog.Entry) bool + // MatchesEmptyRecord returns true if an empty record (without any entries) + // satisfies the predicate. + MatchesEmptyRecord(klog.Record) bool } type IsInDateRange struct { - From klog.Date - To klog.Date + From klog.Date // May be nil to denote open range. + To klog.Date // May be nil to denote open range. +} + +func (i IsInDateRange) Matches(r klog.Record, e klog.Entry) bool { + return i.MatchesEmptyRecord(r) } -func (i IsInDateRange) Matches(e queriedEntry) bool { +func (i IsInDateRange) MatchesEmptyRecord(r klog.Record) bool { isAfter := func() bool { if i.From == nil { return true } - return e.parent.Date().IsAfterOrEqual(i.From) + return r.Date().IsAfterOrEqual(i.From) }() isBefore := func() bool { if i.To == nil { return true } - return i.To.IsAfterOrEqual(e.parent.Date()) + return i.To.IsAfterOrEqual(r.Date()) }() return isAfter && isBefore } @@ -41,17 +45,30 @@ type HasTag struct { Tag klog.Tag } -func (h HasTag) Matches(e queriedEntry) bool { - return e.parent.Summary().Tags().Contains(h.Tag) || e.entry.Summary().Tags().Contains(h.Tag) +func (h HasTag) Matches(r klog.Record, e klog.Entry) bool { + return h.MatchesEmptyRecord(r) || e.Summary().Tags().Contains(h.Tag) +} + +func (h HasTag) MatchesEmptyRecord(r klog.Record) bool { + return r.Summary().Tags().Contains(h.Tag) } type And struct { Predicates []Predicate } -func (a And) Matches(e queriedEntry) bool { +func (a And) Matches(r klog.Record, e klog.Entry) bool { + for _, p := range a.Predicates { + if !p.Matches(r, e) { + return false + } + } + return true +} + +func (a And) MatchesEmptyRecord(r klog.Record) bool { for _, p := range a.Predicates { - if !p.Matches(e) { + if !p.MatchesEmptyRecord(r) { return false } } @@ -62,9 +79,18 @@ type Or struct { Predicates []Predicate } -func (o Or) Matches(e queriedEntry) bool { +func (o Or) Matches(r klog.Record, e klog.Entry) bool { for _, p := range o.Predicates { - if p.Matches(e) { + if p.Matches(r, e) { + return true + } + } + return false +} + +func (o Or) MatchesEmptyRecord(r klog.Record) bool { + for _, p := range o.Predicates { + if p.MatchesEmptyRecord(r) { return true } } @@ -75,8 +101,12 @@ type Not struct { Predicate Predicate } -func (n Not) Matches(e queriedEntry) bool { - return !n.Predicate.Matches(e) +func (n Not) Matches(r klog.Record, e klog.Entry) bool { + return !n.Predicate.Matches(r, e) +} + +func (n Not) MatchesEmptyRecord(r klog.Record) bool { + return !n.Predicate.MatchesEmptyRecord(r) } type EntryType string @@ -108,17 +138,17 @@ type IsEntryType struct { Type EntryType } -func (t IsEntryType) Matches(e queriedEntry) bool { - return klog.Unbox[bool](&e.entry, func(r klog.Range) bool { +func (t IsEntryType) Matches(r klog.Record, e klog.Entry) bool { + return klog.Unbox[bool](&e, func(r klog.Range) bool { return t.Type == ENTRY_TYPE_RANGE }, func(duration klog.Duration) bool { if t.Type == ENTRY_TYPE_DURATION { return true } - if t.Type == ENTRY_TYPE_DURATION_POSITIVE && e.entry.Duration().InMinutes() >= 0 { + if t.Type == ENTRY_TYPE_DURATION_POSITIVE && e.Duration().InMinutes() >= 0 { return true } - if t.Type == ENTRY_TYPE_DURATION_NEGATIVE && e.entry.Duration().InMinutes() < 0 { + if t.Type == ENTRY_TYPE_DURATION_NEGATIVE && e.Duration().InMinutes() < 0 { return true } return false @@ -126,3 +156,7 @@ func (t IsEntryType) Matches(e queriedEntry) bool { return t.Type == ENTRY_TYPE_OPEN_RANGE }) } + +func (t IsEntryType) MatchesEmptyRecord(r klog.Record) bool { + return false +} diff --git a/klog/service/warning.go b/klog/service/warning.go index d2eec5e..e1cfa20 100644 --- a/klog/service/warning.go +++ b/klog/service/warning.go @@ -7,6 +7,23 @@ import ( "github.com/jotaen/klog/klog" ) +// UsageWarning contains information for avoiding potential usage issues. +type UsageWarning struct { + Name string + Message string +} + +var ( + PointlessNowWarning = UsageWarning{ + Name: "POINTLESS_NOW", + Message: "You specified --now, but there was no open-ended time range", + } + DiffEntryFilteringWarning = UsageWarning{ + Name: "ENTRY_FILTERED_DIFFING", + Message: "Combining --diff and filtering at entry-level may yield nonsensical results", + } +) + type checker interface { Warn(klog.Record) klog.Date Message() string @@ -23,6 +40,8 @@ func NewDisabledCheckers() DisabledCheckers { (&futureEntriesChecker{}).Name(): false, (&overlappingTimeRangesChecker{}).Name(): false, (&moreThan24HoursChecker{}).Name(): false, + PointlessNowWarning.Name: false, + DiffEntryFilteringWarning.Name: false, } }