Skip to content

Commit e8e5f43

Browse files
authored
Make filter built in work on dicts (#188)
1 parent 4a7e3f0 commit e8e5f43

11 files changed

Lines changed: 106 additions & 20 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- `match ... end` pattern matching syntax with value matching, type matching, `_` wildcard, maybe destructuring (`just v`/`none`), list destructuring (`[a b ...rest]`), and dict destructuring (`{ 'key': v }`)
1313
- `map` on dictionaries (maps over values, preserving keys)
1414
- Functions
15+
- `filter` builtin now supports dictionaries, filtering by value while preserving keys
1516
- `cdh`
1617
- `cdp`
1718
- `fromUnixTimeMicro`

doc/control-flow.inc.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,12 @@ <h1 id="control-flow-prefix-quote">Prefix Quote Syntax <a class="section-link" h
342342
<span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellINTEGER">1</span> <span class="mshellINTEGER">2</span> <span class="mshellINTEGER">3</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellINTEGER">4</span> <span class="mshellINTEGER">5</span> <span class="mshellINTEGER">6</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellINTEGER">7</span> <span class="mshellINTEGER">8</span> <span class="mshellINTEGER">9</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellPREFIXQUOTE">map.</span> <span class="mshellPREFIXQUOTE">filter.</span> <span class="mshellINTEGER">5</span> <span class="mshellGREATERTHAN">&gt;</span> <span class="mshellEND">end</span> <span class="mshellEND">end</span> <span class="mshellLINECOMMENT"># Result: [[] [6] [7 8 9]]</span></code>
343343
</pre>
344344

345+
<p><code>filter</code> also works on dictionaries; the quotation receives each value and matching entries keep their original keys:</p>
346+
347+
<pre>
348+
<code><span class="mshellLEFT_CURLY">{</span> <span class="mshellSTRING">"a"</span><span class="mshellCOLON">:</span> <span class="mshellINTEGER">1</span><span class="mshellCOMMA">,</span> <span class="mshellSTRING">"b"</span><span class="mshellCOLON">:</span> <span class="mshellINTEGER">2</span><span class="mshellCOMMA">,</span> <span class="mshellSTRING">"c"</span><span class="mshellCOLON">:</span> <span class="mshellINTEGER">3</span> <span class="mshellRIGHT_CURLY">}</span> <span class="mshellPREFIXQUOTE">filter.</span> <span class="mshellINTEGER">1</span> <span class="mshellGREATERTHAN">&gt;</span> <span class="mshellEND">end</span> <span class="mshellLITERAL">str</span> <span class="mshellLITERAL">wl</span> <span class="mshellLINECOMMENT"># {"b": 2, "c": 3}</span></code>
349+
</pre>
350+
345351
<p>
346352
This is also useful to turn boolean operators <code>and</code> and <code>or</code> into
347353
a more traditional lookup infix format:

doc/functions.inc.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ <h1 id="functions-lists">Lists <a class="section-link" href="#functions-lists" a
237237
<tr> <td><code>nth</code></td> <td>Return the nth element (0-based).</td> <td><code>([a] <span class="sig-type sig-type-int">int</span> -- a)</code></td> </tr>
238238
<tr> <td><code>reverse</code></td> <td>Reverse a list.</td> <td><code>(<span class="sig-type sig-type-list">list</span> -- <span class="sig-type sig-type-list">list</span>)</code></td> </tr>
239239
<tr> <td><code>sum</code></td> <td>Sum numeric values in a list.</td> <td><code>([<span class="sig-type sig-type-numeric">numeric</span>] -- <span class="sig-type sig-type-numeric">numeric</span>)</code></td> </tr>
240-
<tr> <td><code>filter</code></td> <td>Filter a list by a predicate quotation.</td> <td><code>([a] (a -- <span class="sig-type sig-type-bool">bool</span>) -- [a])</code></td> </tr>
240+
<tr> <td><code>filter</code></td> <td>Filter a list or dictionary by a predicate quotation, returning a new list or dictionary. The input collection is not modified in place. For dictionaries, the quotation is applied to each value and matching entries are preserved.</td> <td><code>([a] (a -- <span class="sig-type sig-type-bool">bool</span>) -- [a])</code>, <code>(<span class="sig-type sig-type-dict">dict</span> (a -- <span class="sig-type sig-type-bool">bool</span>) -- <span class="sig-type sig-type-dict">dict</span>)</code></td> </tr>
241241
<tr> <td><code>linearSearch</code></td> <td>Return the first element that satisfies a predicate quotation, or <code>none</code> when missing.</td> <td><code>([a] (a -- <span class="sig-type sig-type-bool">bool</span>) -- <span class="sig-type sig-type-maybe">Maybe</span>[a])</code></td> </tr>
242242
<tr> <td><code>any</code></td> <td>Return true if any element satisfies the predicate.</td> <td><code>([a] (a -- <span class="sig-type sig-type-bool">bool</span>) -- <span class="sig-type sig-type-bool">bool</span>)</code></td> </tr>
243243
<tr> <td><code>all</code></td> <td>Return true if all elements satisfy the predicate.</td> <td><code>([a] (a -- <span class="sig-type sig-type-bool">bool</span>) -- <span class="sig-type sig-type-bool">bool</span>)</code></td> </tr>

doc/mshell.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ end
741741
- `nth`: Nth element of list (0-based) `([a] int -- a)`
742742
- `reverse`: Reverse list, `(list -- list)`
743743
- `sum`: Sum of list, `([numeric] -- numeric)`
744-
- `filter`: Filter list, `([a] (a -- bool) -- [a])`
744+
- `filter`: Filter a list or dictionary, returning a new collection. The input list or dictionary is not modified in place. For dictionaries, the quotation is applied to each value and matching entries are preserved. `([a] (a -- bool) -- [a])`, `(dict (a -- bool) -- dict)`
745745
- `any`: Check if any element in list satisfies a condition, `([a] (a -- bool) -- bool)`
746746
- `all`: Check if all elements in list satisfy a condition, `([a] (a -- bool) -- bool)`
747747
- `skip`: Skip first n elements of list, `(list int -- list)`
@@ -779,6 +779,7 @@ end
779779
- `values`: Get values from dictionary. Sorted. `(dict -- [str])`
780780
- `keyValues`: Get key/value pairs from dictionary as a list of lists. Each inner list is a two-element list with the key and value. Sorted by key. `(dict -- [[str a]])`
781781
- `map`: Map a quotation over dictionary values. Keys are preserved. `(dict (a -- b) -- dict)`
782+
- `filter`: Filter dictionary values with a predicate quotation, returning a new dictionary. Keys are preserved for matching entries, and the original dictionary is not modified in place. `(dict (a -- bool) -- dict)`
782783
- `in`: Check if key exists in dictionary. `(dict str -- bool)`
783784

784785
## Date Functions

lib/std.msh

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -109,24 +109,6 @@ def dropWhile ([T] (T -- bool) -- [T])
109109
@list @dropWhile-idx skip
110110
end
111111

112-
# filter (list quote -- list)
113-
def filter ([T] (T -- bool) -- [T])
114-
list!, quote!
115-
@list len filter-len! # Get total length
116-
0 filter-idx! # Index
117-
[] filter-accum! # Accumulator
118-
(
119-
@filter-idx @filter-len >= (break) iff
120-
@list @filter-idx nth item!
121-
# $"Item: {@item str}" wl
122-
@item @quote x result!
123-
# $"Result: {@result str}" wl
124-
@result ( @filter-accum @item append drop ) iff
125-
@filter-idx 1 + filter-idx! # inc index
126-
) loop
127-
@filter-accum
128-
end
129-
130112
def linearSearch ([T] (T -- bool) -- Maybe[T])
131113
list!, quote!
132114
none linearSearch-result!

mshell/BuiltInList.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ var BuiltInList = map[string]struct{}{
4545
"fileExists": {},
4646
"fileSize": {},
4747
"files": {},
48+
"filter": {},
4849
"findReplace": {},
4950
"floatCmp": {},
5051
"floor": {},

mshell/Evaluator.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4651,6 +4651,83 @@ MainLoop:
46514651
return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'just' operation on an empty stack.\n", t.Line, t.Column))
46524652
}
46534653
stack.Push(&Maybe{obj: o})
4654+
} else if t.Lexeme == "filter" {
4655+
obj1, obj2, err := stack.Pop2(t)
4656+
if err != nil {
4657+
return state.FailWithMessage(err.Error())
4658+
}
4659+
4660+
fn, ok := obj1.(*MShellQuotation)
4661+
if !ok {
4662+
return state.FailWithMessage(fmt.Sprintf("%d:%d: The first parameter in 'filter' is expected to be a function, found a %s (%s)\n", t.Line, t.Column, obj1.TypeName(), obj1.DebugString()))
4663+
}
4664+
4665+
switch obj2Typed := obj2.(type) {
4666+
case *MShellList:
4667+
listObj := obj2Typed
4668+
newList := NewList(0)
4669+
4670+
var filterStack MShellStack
4671+
filterStack = []MShellObject{}
4672+
4673+
for _, item := range listObj.Items {
4674+
filterStack.Push(item)
4675+
result, err := state.EvaluateQuote(*fn, &filterStack, context, definitions)
4676+
if err != nil {
4677+
return state.FailWithMessage(err.Error())
4678+
}
4679+
if result.ShouldPassResultUpStack() {
4680+
return result
4681+
}
4682+
if len(filterStack) != 1 {
4683+
return state.FailWithMessage(fmt.Sprintf("%d:%d: The function in 'filter' did not return a single value, found %d values.\n", t.Line, t.Column, len(filterStack)))
4684+
}
4685+
4686+
filterResult, _ := filterStack.Pop()
4687+
filterBool, ok := filterResult.(MShellBool)
4688+
if !ok {
4689+
return state.FailWithMessage(fmt.Sprintf("%d:%d: The function in 'filter' did not return a bool, found a %s (%s).\n", t.Line, t.Column, filterResult.TypeName(), filterResult.DebugString()))
4690+
}
4691+
4692+
if filterBool.Value {
4693+
newList.Items = append(newList.Items, item)
4694+
}
4695+
}
4696+
stack.Push(newList)
4697+
case *MShellDict:
4698+
dictObj := obj2Typed
4699+
newDict := &MShellDict{Items: make(map[string]MShellObject, len(dictObj.Items))}
4700+
4701+
var filterStack MShellStack
4702+
filterStack = []MShellObject{}
4703+
4704+
for key, value := range dictObj.Items {
4705+
filterStack.Push(value)
4706+
result, err := state.EvaluateQuote(*fn, &filterStack, context, definitions)
4707+
if err != nil {
4708+
return state.FailWithMessage(err.Error())
4709+
}
4710+
if result.ShouldPassResultUpStack() {
4711+
return result
4712+
}
4713+
if len(filterStack) != 1 {
4714+
return state.FailWithMessage(fmt.Sprintf("%d:%d: The function in 'filter' did not return a single value, found %d values.\n", t.Line, t.Column, len(filterStack)))
4715+
}
4716+
4717+
filterResult, _ := filterStack.Pop()
4718+
filterBool, ok := filterResult.(MShellBool)
4719+
if !ok {
4720+
return state.FailWithMessage(fmt.Sprintf("%d:%d: The function in 'filter' did not return a bool, found a %s (%s).\n", t.Line, t.Column, filterResult.TypeName(), filterResult.DebugString()))
4721+
}
4722+
4723+
if filterBool.Value {
4724+
newDict.Items[key] = value
4725+
}
4726+
}
4727+
stack.Push(newDict)
4728+
default:
4729+
return state.FailWithMessage(fmt.Sprintf("%d:%d: The second parameter in 'filter' is expected to be a list or dictionary, found a %s (%s)\n", t.Line, t.Column, obj2.TypeName(), obj2.DebugString()))
4730+
}
46544731
} else if t.Lexeme == "map" {
46554732
// Map a function over a list, or a Maybe
46564733
obj1, obj2, err := stack.Pop2(t)

tests/filter.msh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,13 @@
22

33
# Test filtering list of lists, this caught a bug with the order of the internal append in filter
44
[[1 2] [] [3 4 5]] (len 1 >) filter ((str) map ", " join) map uw
5+
6+
{ "a": 1, "b": 2, "c": 3 } (1 >) filter str wl
7+
8+
[1 2 3 4] originalList!
9+
@originalList (2 >) filter str wl
10+
@originalList str wl
11+
12+
{ "a": 1, "b": 2, "c": 3 } originalDict!
13+
@originalDict (1 >) filter str wl
14+
@originalDict str wl

tests/filter.msh.stdout

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
4, 5
22
1, 2
33
3, 4, 5
4+
{"b": 2, "c": 3}
5+
[3 4]
6+
[1 2 3 4]
7+
{"b": 2, "c": 3}
8+
{"a": 1, "b": 2, "c": 3}

tests/prefix_quote.msh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ true or. false end and. true end str wl
1616
5 threshold!
1717
[1 2 3 4 5 6 7] filter. @threshold > end (str) map ", " join wl
1818

19+
{ "a": 1, "b": 2, "c": 3 } filter. 1 > end str wl
20+
1921
# Prefix quote calling user-defined function
2022
def double (int -- int) 2 * end
2123
[1 2 3] map. double end (str) map ", " join wl

0 commit comments

Comments
 (0)