Author: Kevin Schoon [kevinschoon@gmail.com]
Hash: f436333d84048aaf09bcd5b0482ac7d43b16309e
Timestamp: Sun, 25 Apr 2021 17:49:44 +0000 (3 years ago)

+111 -88 +/-3 browse
simplify filtering
1diff --git a/lib/cmd.ml b/lib/cmd.ml
2index e2d2843..c402032 100644
3--- a/lib/cmd.ml
4+++ b/lib/cmd.ml
5 @@ -9,18 +9,20 @@ let get_notes =
6 Note.of_string ~content slug)
7 (Slug.load cfg.state_dir)
8
9- let filter_arg =
10+ let to_keys ~kind notes =
11+ match kind with
12+ | `Title -> List.map ~f:Note.get_title notes
13+ | `Tags -> List.concat (List.map ~f:Note.get_tags notes)
14+
15+ let search_arg kind =
16 Command.Arg_type.create
17 ~complete:(fun _ ~part ->
18 let notes = get_notes in
19 List.filter_map
20- ~f:(fun note ->
21- let title = Note.get_title note in
22- if String.equal part "" then Some title
23- else if String.is_substring ~substring:part title then Some title
24- else None)
25- notes)
26- (fun filter -> filter)
27+ ~f:(fun key ->
28+ if String.is_substring ~substring:part key then Some key else None)
29+ (to_keys ~kind notes))
30+ (fun filter -> Re.Str.regexp filter)
31
32 let key_arg =
33 Command.Arg_type.create
34 @@ -31,6 +33,8 @@ let key_arg =
35 string_keys)
36 Config.Key.of_string
37
38+ let flag_to_op state = match state with true -> Note.And | false -> Note.Or
39+
40 let column_list_arg =
41 Command.Arg_type.create (fun value ->
42 List.map ~f:Config.Column.of_string (String.split ~on:',' value))
43 @@ -57,6 +61,20 @@ let list_style_arg =
44 string_keys)
45 Config.ListStyle.of_string
46
47+ let filter_args =
48+ let open Command.Let_syntax in
49+ [%map_open
50+ let title =
51+ flag "title"
52+ (optional (search_arg `Title))
53+ ~doc:"regular expression matching the note title"
54+ and tags =
55+ flag "tag"
56+ (listed (search_arg `Tags))
57+ ~doc:"sequence of regular expressions matching note tags"
58+ and operator = flag "and" no_arg ~doc:"logical AND instead of default OR" in
59+ (title, tags, operator)]
60+
61 (*
62 * commands
63 *)
64 @@ -71,15 +89,18 @@ note to stdout as plain text however the encoding can be adjusted to yaml or
65 json for consumption by other tools.
66 |})
67 [%map_open
68- let filter_args = anon (sequence ("filter" %: filter_arg))
69+ let title, tags, operator = filter_args
70 and encoding =
71 flag "encoding"
72 (optional_with_default cfg.encoding encoding_arg)
73 ~doc:"format [json | yaml | raw] (default: raw)"
74 in
75 fun () ->
76- let open Note.Search in
77- let notes = find_many ~args:filter_args get_notes in
78+ let notes =
79+ Note.find_many
80+ ~term:{ title; tags; operator = flag_to_op operator }
81+ get_notes
82+ in
83 List.iter
84 ~f:(fun note ->
85 print_endline (Note.Encoding.to_string ~style:encoding note))
86 @@ -142,11 +163,14 @@ let delete_note =
87 Delete the first note that matches the filter criteria.
88 |})
89 [%map_open
90- let filter_args = anon (sequence ("filter" %: filter_arg)) in
91+ let title, tags, operator = filter_args in
92 fun () ->
93- let open Note.Search in
94 let notes = get_notes in
95- let note = find_one ~args:filter_args notes in
96+ let note =
97+ Note.find_one
98+ ~term:{ title; tags; operator = flag_to_op operator }
99+ notes
100+ in
101 match note with
102 | Some note ->
103 Io.delete ~callback:cfg.on_modification ~title:(Note.get_title note)
104 @@ -161,10 +185,13 @@ let edit_note =
105 Select a note that matches the filter criteria and open it in your text editor.
106 |})
107 [%map_open
108- let filter_args = anon (sequence ("filter" %: filter_arg)) in
109+ let title, tags, operator = filter_args in
110 fun () ->
111- let open Note.Search in
112- let note = find_one ~args:filter_args get_notes in
113+ let note =
114+ Note.find_one
115+ ~term:{ title; tags; operator = flag_to_op operator }
116+ get_notes
117+ in
118 match note with
119 | Some note ->
120 Io.edit ~callback:cfg.on_modification ~editor:cfg.editor
121 @@ -180,7 +207,7 @@ List one or more notes that match the filter criteria, if no filter criteria
122 is provided then all notes will be listed.
123 |})
124 [%map_open
125- let filter_args = anon (sequence ("filter" %: filter_arg))
126+ let title, tags, operator = filter_args
127 and style =
128 flag "style"
129 (optional_with_default cfg.list_style list_style_arg)
130 @@ -191,8 +218,11 @@ is provided then all notes will be listed.
131 ~doc:"columns to include in output"
132 in
133 fun () ->
134- let open Note.Search in
135- let notes = find_many ~args:filter_args get_notes in
136+ let notes =
137+ Note.find_many
138+ ~term:{ title; tags; operator = flag_to_op operator }
139+ get_notes
140+ in
141 let styles = cfg.styles in
142 let cells = Note.to_cells ~columns ~styles notes in
143 Display.to_stdout ~style cells]
144 diff --git a/lib/note.ml b/lib/note.ml
145index 7ca26a1..4b0e286 100644
146--- a/lib/note.ml
147+++ b/lib/note.ml
148 @@ -1,11 +1,19 @@
149 open Core
150
151- type t = {
152+ type operator = And | Or
153+
154+ and term = {
155+ title : Re.Str.regexp option;
156+ tags : Re.Str.regexp list;
157+ operator : operator;
158+ }
159+
160+ and note = {
161 frontmatter : Ezjsonm.t;
162 content : string;
163 markdown : Omd.doc;
164 slug : Slug.t;
165- parent : Slug.t option;
166+ parent : term option;
167 }
168
169 let build ?(description = "") ?(tags = []) ?(content = "") ~title slug =
170 @@ -133,61 +141,39 @@ module Encoding = struct
171 | `Html -> Omd.to_html t.markdown
172 end
173
174- module Search = struct
175- open Re.Str
176-
177- let dump_results results =
178- List.iter
179- ~f:(fun result ->
180- print_endline (sprintf "%s - %d" (get_title (snd result)) (fst result)))
181- results
182-
183- let title expr note =
184- let title_string = get_title note in
185- string_match expr title_string 0
186-
187- let tags expr note =
188- let tags = get_tags note in
189- List.count ~f:(fun tag -> string_match expr tag 0) tags > 0
190-
191- let content expr note =
192- let words = Util.to_words [] note.markdown in
193- List.count ~f:(fun word -> string_match expr word 0) words > 0
194-
195- let match_and_rank ~args notes =
196- let expressions = List.map ~f:regexp args in
197- let matches =
198- List.fold ~init:[]
199- ~f:(fun accm note ->
200- let has_title =
201- List.count ~f:(fun expr -> title expr note) expressions > 0
202- in
203- let has_tag =
204- List.count ~f:(fun expr -> tags expr note) expressions > 0
205- in
206- let has_content =
207- List.count ~f:(fun expr -> content expr note) expressions > 0
208+ let find_many ~term notes =
209+ let open Re.Str in
210+ if Option.is_none term.title && List.length term.tags = 0 then notes
211+ else
212+ List.filter
213+ ~f:(fun note ->
214+ let has_title =
215+ match term.title with
216+ | Some title -> string_match title (get_title note) 0
217+ | None -> false
218+ in
219+ let has_title = Option.is_none term.title || has_title in
220+ let has_tags =
221+ let result =
222+ List.find
223+ ~f:(fun expr ->
224+ Option.is_some
225+ (List.find
226+ ~f:(fun tag -> string_match expr tag 0)
227+ (get_tags note)))
228+ term.tags
229 in
230- match (has_title, has_tag, has_content) with
231- | true, _, _ -> List.append accm [ (20, note) ]
232- | _, true, _ -> List.append accm [ (10, note) ]
233- | _, _, true -> List.append accm [ (5, note) ]
234- | false, false, false -> accm)
235- notes
236- in
237- List.rev (List.sort ~compare:(fun n1 n2 -> fst n1 - fst n2) matches)
238-
239- let find_one ~args notes =
240- let results = match_and_rank ~args notes in
241- let results = List.map ~f:snd results in
242- if List.length results = 0 then None else Some (List.hd_exn results)
243-
244- let find_many ~args notes =
245- if List.length args = 0 then notes
246- else
247- let results = match_and_rank ~args notes in
248- List.map ~f:snd results
249- end
250+ Option.is_some result
251+ in
252+ let has_tags = List.length term.tags = 0 || has_tags in
253+ match term.operator with
254+ | Or -> has_title || has_tags
255+ | And -> has_title && has_tags)
256+ notes
257+
258+ let find_one ~term notes =
259+ let results = find_many ~term notes in
260+ match List.length results with 0 -> None | _ -> Some (List.hd_exn results)
261
262 open ANSITerminal
263
264 diff --git a/test/search.ml b/test/search.ml
265index 5a47765..e7cb68a 100644
266--- a/test/search.ml
267+++ b/test/search.ml
268 @@ -11,31 +11,38 @@ let make_notes =
269 ~title:"Another Very Important Note" (Slug.next state_dir)
270 in
271 let note_3 =
272- Note.build ~tags:[ "fuu"; "bar"; "baz" ] ~content:""
273- ~title:"fuu" (Slug.next state_dir)
274+ Note.build ~tags:[ "fuu"; "bar"; "baz" ] ~content:"" ~title:"fuu"
275+ (Slug.next state_dir)
276 in
277- [note_1 ; note_2 ; note_3]
278-
279+ [ note_1; note_2; note_3 ]
280
281 let test_filter_by_keys =
282 let notes = make_notes in
283 let result =
284- Note.Search.find_many ~args:[ "fuu"; "bar"; "baz" ]
285- notes
286+ Note.find_many
287+ ~term:
288+ {
289+ title = None;
290+ tags =
291+ [ Re.Str.regexp "fuu"; Re.Str.regexp "bar"; Re.Str.regexp "baz" ];
292+ operator = Note.Or;
293+ }
294+ notes
295 in
296 assert (List.length result = 3)
297
298 let test_filter_by_title_find_one =
299 let notes = make_notes in
300 let result =
301- Note.Search.find_one ~args:[ "fuu" ]
302- notes
303+ Note.find_one
304+ ~term:{ title = None; tags = [ Re.Str.regexp "fuu" ]; operator = Note.Or }
305+ notes
306 in
307- assert (Option.is_some result) ;
308+ assert (Option.is_some result);
309 let note = Option.get result in
310 (* title should take priority *)
311- assert ((Note.get_title note) = "fuu")
312+ assert (Note.get_title note = "fuu")
313
314- let () =
315+ let () =
316 test_filter_by_keys;
317- test_filter_by_title_find_one;
318+ test_filter_by_title_find_one