Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions klog/app/cli/args/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/jotaen/klog/klog"
"github.com/jotaen/klog/klog/app"
"github.com/jotaen/klog/klog/service"
"github.com/jotaen/klog/klog/service/kfl"
"github.com/jotaen/klog/klog/service/period"
)

Expand Down Expand Up @@ -33,6 +34,8 @@ type FilterArgs struct {
LastQuarter bool `hidden:"" name:"last-quarter" group:"Filter"`
ThisYear bool `hidden:"" name:"this-year" group:"Filter"`
LastYear bool `hidden:"" name:"last-year" group:"Filter"`

FilterQuery string `name:"filter" placeholder:"KQL-FILTER-QUERY" group:"Filter" help:"(Experimental)"`
}

// FilterArgsCompletionOverrides enables/disables tab completion for
Expand All @@ -51,6 +54,19 @@ var FilterArgsCompletionOverrides = map[string]bool{
}

func (args *FilterArgs) ApplyFilter(now gotime.Time, rs []klog.Record) ([]klog.Record, app.Error) {
if args.FilterQuery != "" {
predicate, err := kfl.Parse(args.FilterQuery)
if err != nil {
return nil, app.NewErrorWithCode(
app.GENERAL_ERROR,
"Malformed filter query",
err.Error(),
err,
)
}
rs = kfl.Filter(predicate, rs)
return rs, nil
}
today := klog.NewDateFromGo(now)
qry := service.FilterQry{
BeforeOrEqual: args.Until,
Expand Down
49 changes: 49 additions & 0 deletions klog/service/kfl/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package kfl

import (
"fmt"
"math"
"strings"

tf "github.com/jotaen/klog/lib/terminalformat"
)

type ParseError interface {
error
Original() error
Position() (int, int)
}

type parseError struct {
err error
position int
length int
query string
}

func (e parseError) Error() string {
errorLength := max(e.length, 1)
relevantQueryFragment, newStart := tf.TextSubstrWithContext(e.query, e.position, errorLength, 10, 20)
return fmt.Sprintf(
"%s\n\n%s\n%s%s%s\nCursor positions %d-%d in query.",
e.err,
relevantQueryFragment,
strings.Repeat("—", max(0, newStart)),
strings.Repeat("^", max(0, errorLength)),
strings.Repeat("—", max(0, len(relevantQueryFragment)-(newStart+errorLength))),
e.position,
e.position+errorLength,
)
}

func (e parseError) Original() error {
return e.err
}

func (e parseError) Position() (int, int) {
return e.position, e.length
}

func max(x int, y int) int {
return int(math.Max(float64(x), float64(y)))
}
23 changes: 23 additions & 0 deletions klog/service/kfl/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kfl

import (
"github.com/jotaen/klog/klog"
)

func Filter(p Predicate, rs []klog.Record) []klog.Record {
var res []klog.Record
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(es) == 0 {
continue
}
r.SetEntries(es)
res = append(res, r)
}
return res
}
154 changes: 154 additions & 0 deletions klog/service/kfl/filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package kfl

import (
"testing"

"github.com/jotaen/klog/klog"
"github.com/jotaen/klog/klog/service"
"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
}(),
}
}

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...))
}

func TestQueryWithAtDate(t *testing.T) {
rs := 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...))
}

func TestQueryWithAfter(t *testing.T) {
rs := 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())
}

func TestQueryWithBefore(t *testing.T) {
rs := 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())
}

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 := 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 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...))
}

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...))
}

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...))
}

func TestQueryWithTagNonMatchingValues(t *testing.T) {
rs := Filter(HasTag{klog.NewTagOrPanic("bar", "3")}, sampleRecordsForQuerying())
require.Len(t, rs, 0)
}

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 := Filter(IsEntryType{ENTRY_TYPE_NEGATIVE_DURATION}, 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 := Filter(IsEntryType{ENTRY_TYPE_POSITIVE_DURATION}, sampleRecordsForQuerying())
require.Len(t, rs, 4)
assert.Equal(t, klog.NewDuration(0, 1575), service.Total(rs...))
}
{
rs := Filter(IsEntryType{ENTRY_TYPE_RANGE}, sampleRecordsForQuerying())
require.Len(t, rs, 0)
assert.Equal(t, klog.NewDuration(0, 0), service.Total(rs...))
}
{
rs := Filter(IsEntryType{ENTRY_TYPE_OPEN_RANGE}, sampleRecordsForQuerying())
require.Len(t, rs, 1)
assert.Equal(t, klog.NewDuration(0, 0), service.Total(rs...))
}
}
Loading