Sort list matches

pull/6/head
Mickaël Menu 3 years ago
parent 09d688f1cc
commit 16fea38764
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -149,8 +149,8 @@ func (d *NoteDAO) exists(path string) (bool, error) {
func (d *NoteDAO) Find(opts note.FinderOpts, callback func(note.Match) error) (int, error) {
rows, err := func() (*sql.Rows, error) {
snippetCol := `""`
orderTerm := `n.title ASC`
whereExprs := make([]string, 0)
orderTerms := make([]string, 0)
args := make([]interface{}, 0)
for _, filter := range opts.Filters {
@ -158,7 +158,7 @@ func (d *NoteDAO) Find(opts note.FinderOpts, callback func(note.Match) error) (i
case note.MatchFilter:
snippetCol = `snippet(notes_fts, 2, '<zk:match>', '</zk:match>', '…', 20) as snippet`
orderTerm = `bm25(notes_fts, 1000.0, 500.0, 1.0)`
orderTerms = append(orderTerms, `bm25(notes_fts, 1000.0, 500.0, 1.0)`)
whereExprs = append(whereExprs, "notes_fts MATCH ?")
args = append(args, fts5.ConvertQuery(string(filter)))
@ -197,10 +197,15 @@ func (d *NoteDAO) Find(opts note.FinderOpts, callback func(note.Match) error) (i
args = append(args, filter.Date)
default:
panic("unknown filter type")
panic(fmt.Sprintf("%v: unknown filter type", filter))
}
}
for _, sorter := range opts.Sorters {
orderTerms = append(orderTerms, orderTerm(sorter))
}
orderTerms = append(orderTerms, `n.title ASC`)
query := "SELECT n.id, n.path, n.title, n.body, n.word_count, n.created, n.modified, n.checksum, " + snippetCol
query += `
@ -212,7 +217,7 @@ ON n.id = notes_fts.rowid`
query += "\nWHERE " + strings.Join(whereExprs, "\nAND ")
}
query += "\nORDER BY " + orderTerm
query += "\nORDER BY " + strings.Join(orderTerms, ", ")
if opts.Limit > 0 {
query += fmt.Sprintf("\nLIMIT %d", opts.Limit)
@ -267,7 +272,7 @@ func dateField(filter note.DateFilter) string {
case note.DateModified:
return "modified"
default:
panic("unknown DateFilter field")
panic(fmt.Sprintf("%v: unknown note.DateField", filter.Field))
}
}
@ -280,6 +285,30 @@ func dateDirection(filter note.DateFilter) (op string, ignoreTime bool) {
case note.DateAfter:
return ">=", false
default:
panic("unknown DateFilter direction")
panic(fmt.Sprintf("%v: unknown note.DateDirection", filter.Direction))
}
}
func orderTerm(sorter note.Sorter) string {
order := " ASC"
if !sorter.Ascending {
order = " DESC"
}
switch sorter.Term {
case note.SortCreated:
return "n.created" + order
case note.SortModified:
return "n.modified" + order
case note.SortPath:
return "n.path" + order
case note.SortRandom:
return "RANDOM()"
case note.SortTitle:
return "n.title" + order
case note.SortWordCount:
return "n.word_count" + order
default:
panic(fmt.Sprintf("%v: unknown note.SortTerm", sorter.Term))
}
}

@ -189,18 +189,6 @@ func TestNoteDAOFindMatch(t *testing.T) {
Checksum: "qwfpgj",
},
},
{
Snippet: "A second <zk:match>daily</zk:match> note",
Metadata: note.Metadata{
Path: "log/2021-01-04.md",
Title: "January 4, 2021",
Body: "A second daily note",
WordCount: 4,
Created: time.Date(2020, 11, 29, 8, 20, 18, 0, time.UTC),
Modified: time.Date(2020, 11, 29, 8, 20, 18, 0, time.UTC),
Checksum: "arstde",
},
},
{
Snippet: "A third <zk:match>daily</zk:match> note",
Metadata: note.Metadata{
@ -213,6 +201,18 @@ func TestNoteDAOFindMatch(t *testing.T) {
Checksum: "earkte",
},
},
{
Snippet: "A second <zk:match>daily</zk:match> note",
Metadata: note.Metadata{
Path: "log/2021-01-04.md",
Title: "January 4, 2021",
Body: "A second daily note",
WordCount: 4,
Created: time.Date(2020, 11, 29, 8, 20, 18, 0, time.UTC),
Modified: time.Date(2020, 11, 29, 8, 20, 18, 0, time.UTC),
Checksum: "arstde",
},
},
},
)
}
@ -343,6 +343,70 @@ func TestNoteDAOFindModifiedAfter(t *testing.T) {
)
}
func TestNoteDAOFindSortCreated(t *testing.T) {
testNoteDAOFindSort(t, note.SortCreated, true, []string{
"ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md",
"log/2021-01-03.md", "log/2021-02-04.md", "log/2021-01-04.md",
})
testNoteDAOFindSort(t, note.SortCreated, false, []string{
"log/2021-02-04.md", "log/2021-01-04.md", "log/2021-01-03.md",
"f39c8.md", "index.md", "ref/test/b.md", "ref/test/a.md",
})
}
func TestNoteDAOFindSortModified(t *testing.T) {
testNoteDAOFindSort(t, note.SortModified, true, []string{
"ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md",
"log/2021-02-04.md", "log/2021-01-03.md", "log/2021-01-04.md",
})
testNoteDAOFindSort(t, note.SortModified, false, []string{
"log/2021-01-04.md", "log/2021-01-03.md", "log/2021-02-04.md",
"f39c8.md", "index.md", "ref/test/b.md", "ref/test/a.md",
})
}
func TestNoteDAOFindSortPath(t *testing.T) {
testNoteDAOFindSort(t, note.SortPath, true, []string{
"f39c8.md", "index.md", "log/2021-01-03.md", "log/2021-01-04.md",
"log/2021-02-04.md", "ref/test/a.md", "ref/test/b.md",
})
testNoteDAOFindSort(t, note.SortPath, false, []string{
"ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md",
"log/2021-01-04.md", "log/2021-01-03.md", "index.md", "f39c8.md",
})
}
func TestNoteDAOFindSortTitle(t *testing.T) {
testNoteDAOFindSort(t, note.SortTitle, true, []string{
"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md",
"index.md", "log/2021-01-03.md", "log/2021-01-04.md",
})
testNoteDAOFindSort(t, note.SortTitle, false, []string{
"log/2021-01-04.md", "log/2021-01-03.md", "index.md",
"log/2021-02-04.md", "ref/test/a.md", "f39c8.md", "ref/test/b.md",
})
}
func TestNoteDAOFindSortWordCount(t *testing.T) {
testNoteDAOFindSort(t, note.SortWordCount, true, []string{
"log/2021-01-03.md", "log/2021-02-04.md", "index.md",
"log/2021-01-04.md", "f39c8.md", "ref/test/a.md", "ref/test/b.md",
})
testNoteDAOFindSort(t, note.SortWordCount, false, []string{
"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md",
"index.md", "log/2021-01-04.md", "log/2021-01-03.md",
})
}
func testNoteDAOFindSort(t *testing.T, term note.SortTerm, ascending bool, expected []string) {
testNoteDAOFindPaths(t,
note.FinderOpts{
Sorters: []note.Sorter{{Term: term, Ascending: ascending}},
},
expected,
)
}
func testNoteDAOFindPaths(t *testing.T, opts note.FinderOpts, expected []string) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
actual := make([]string, 0)

@ -2,7 +2,9 @@ package cmd
import (
"fmt"
"strings"
"time"
"unicode/utf8"
"github.com/mickael-menu/zk/adapter/sqlite"
"github.com/mickael-menu/zk/core/note"
@ -25,6 +27,7 @@ type List struct {
ModifiedBefore string `help:"Show only the notes modified before the given date" placeholder:"DATE"`
ModifiedAfter string `help:"Show only the notes modified after the given date" placeholder:"DATE"`
Exclude []string `help:"Excludes notes matching the given file path pattern from the list" placeholder:"GLOB"`
Sort []string `help:"Sort the notes by the given criterion" short:"s" placeholder:"CRITERION"`
}
func (cmd *List) Run(container *Container) error {
@ -150,10 +153,16 @@ func (cmd *List) ListOpts(zk *zk.Zk) (*note.ListOpts, error) {
})
}
sorters, err := sorters(cmd.Sort)
if err != nil {
return nil, err
}
return &note.ListOpts{
Format: opt.NewNotEmptyString(cmd.Format),
FinderOpts: note.FinderOpts{
Filters: filters,
Sorters: sorters,
Limit: cmd.Limit,
},
}, nil
@ -179,3 +188,67 @@ func parseDate(date string) (time.Time, error) {
// FIXME: support years
return naturaldate.Parse(date, time.Now().UTC(), naturaldate.WithDirection(naturaldate.Past))
}
func sorters(terms []string) ([]note.Sorter, error) {
sorters := make([]note.Sorter, 0)
for _, term := range terms {
orderSymbol, _ := utf8.DecodeLastRuneInString(term)
term = strings.TrimRight(term, "+-")
sorter, err := sorter(term)
if err != nil {
return sorters, err
}
switch orderSymbol {
case '+':
sorter.Ascending = true
case '-':
sorter.Ascending = false
}
sorters = append(sorters, sorter)
}
return sorters, nil
}
func sorter(term string) (sorter note.Sorter, err error) {
switch {
case strings.HasPrefix("created", term):
sorter = note.Sorter{Term: note.SortCreated, Ascending: false}
case strings.HasPrefix("modified", term):
sorter = note.Sorter{Term: note.SortModified, Ascending: false}
case strings.HasPrefix("path", term):
sorter = note.Sorter{Term: note.SortPath, Ascending: true}
case strings.HasPrefix("title", term):
sorter = note.Sorter{Term: note.SortTitle, Ascending: true}
case strings.HasPrefix("random", term):
sorter = note.Sorter{Term: note.SortRandom, Ascending: true}
case strings.HasPrefix("word-count", term):
sorter = note.Sorter{Term: note.SortWordCount, Ascending: true}
default:
err = fmt.Errorf("%s: unknown sorting term", term)
}
return
}
func sortAscending(symbol rune, term string) bool {
switch term {
case "created":
return false
case "modified":
return false
case "path":
return true
case "title":
return true
case "random":
return true
case "word-count":
return true
}
return true
}

@ -43,6 +43,28 @@ const (
DateModified
)
type Sorter struct {
Term SortTerm
Ascending bool
}
type SortTerm int
const (
// Sort by creation date.
SortCreated SortTerm = iota + 1
// Sort by modification date.
SortModified
// Sort by the file paths.
SortPath
// Sort randomly.
SortRandom
// Sort by the note titles.
SortTitle
// Sort by the number of words in the note bodies.
SortWordCount
)
// Match holds information about a note matching the list filters.
type Match struct {
// Snippet is an excerpt of the note.
@ -65,6 +87,7 @@ type Finder interface {
type FinderOpts struct {
Filters []Filter
Sorters []Sorter
Limit int
}

Loading…
Cancel
Save