• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nhartland / forma / 22256968205

21 Feb 2026 12:39PM UTC coverage: 97.368% (-1.0%) from 98.384%
22256968205

Pull #29

github

web-flow
Merge b30cd7abd into 319687f37
Pull Request #29: Dev PR

471 of 487 new or added lines in 15 files covered. (96.71%)

26 existing lines in 4 files now uncovered.

2109 of 2166 relevant lines covered (97.37%)

7563.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.33
/forma/multipattern.lua
1
--- A class contain a collection of `pattern` objects.
2
-- Many pattern operations generate a set of patterns. This
3
-- class aims to provide a convenient collection with some
4
-- common methods for handling them.
5
--
6
-- @module forma.multipattern
7
local multipattern = {}
1✔
8

9

10
-- Multipattern indexing
11
-- For enabling syntax sugar multipattern:method
12
-- This retains the ability to index by number.
13
multipattern.__index = function(mp, key)
14
    if type(key) == "number" then
1,521✔
15
        return mp.components[key]
108✔
16
    else
17
        return multipattern[key]
1,413✔
18
    end
19
end
20

21
--- Multipattern length.
22
-- Note: Only works with lua5.2 and above.
23
-- Returns the number of components in the multipattern.
24
multipattern.__len = function(mp)
25
    return mp:n_components()
×
26
end
27

28

29
--- Create a new multipattern from a list of patterns.
30
-- @param components an array of `pattern` objects.
31
-- @return a new multipattern containing those patterns.
32
function multipattern.new(components)
1✔
33
    local mp = {
59✔
34
        components = components or {}
59✔
35
    }
36
    mp = setmetatable(mp, multipattern)
59✔
37
    return mp
59✔
38
end
39

40
--- Clone the multipattern.
41
-- @param mp multipattern to clone.
42
-- @return the cloned multipattern.
43
function multipattern.clone(mp)
1✔
44
    local components = {}
1✔
45
    for i, p in ipairs(mp.components) do
1,265✔
46
        components[i] = p:clone()
1,264✔
47
    end
48
    return multipattern.new(components)
1✔
49
end
50

51
--- Merge multipatterns.
52
-- @param ... a table of multipatterns or a list of pattern arguments.
53
-- @return a new multipattern that is consists of the set of all input components
54
function multipattern.merge(...)
1✔
55
    local multipatterns = { ... }
1✔
56
    if #multipatterns == 1 then
1✔
NEW
57
        if type(multipatterns[1]) == 'table' then
×
NEW
58
            multipatterns = multipatterns[1]
×
59
        end
60
    end
61
    if #multipatterns == 1 then
1✔
NEW
62
        return multipatterns[1]
×
63
    end
64
    local total = multipattern.new()
1✔
65
    for _, v in ipairs(multipatterns) do
3✔
66
        assert(getmetatable(v) == multipattern, "multipattern.merge requires multipatterns as arguments")
2✔
67
        for _, p in ipairs(v.components) do
5✔
68
            total:insert(p)
3✔
69
        end
70
    end
71
    return total
1✔
72
end
73

74
--- Insert a pattern into the multipattern.
75
-- @param mp multipattern to be operated upon.
76
-- @param ip the new pattern to insert.
77
-- @return the multipattern.
78
function multipattern.insert(mp, ip)
1✔
79
    assert(getmetatable(mp) == multipattern, "multipattern.insert requires a multipattern as the first argument")
1,350✔
80
    table.insert(mp.components, ip)
1,350✔
81
    return mp
1,350✔
82
end
83

84
--- Count the number of components in a multipattern.
85
-- @param mp the multipattern to count.
86
-- @return the number of components.
87
function multipattern.n_components(mp)
1✔
88
    assert(getmetatable(mp) == multipattern, "multipattern.n_components requires a multipattern as the first argument")
36✔
89
    return #mp.components
36✔
90
end
91

92
--- Map a function over all patterns in this multipattern.
93
-- Calls `fn(pattern, index)` for each sub-pattern, returning a new multipattern
94
-- of their results.
95
--
96
-- **Example**:
97
--  ```
98
--  local bigger = mp:map(function(p) return p:enlarge(2) end)
99
--  ```
100
--
101
-- @param mp the multipattern upon which to map the function.
102
-- @param fn a function taking `(pattern, index)` and returning a new `pattern`.
103
-- @return a new multipattern of the mapped results.
104
function multipattern.map(mp, fn)
1✔
105
    assert(getmetatable(mp) == multipattern, "multipattern.map requires a multipattern as an argument")
1✔
106
    -- Applies `fn` to each pattern in this multipattern,
107
    -- returning a new multipattern of results.
108
    -- fn is a function(pat, index) -> (some pattern)
109
    local new_components = {}
1✔
110
    for i, pat in ipairs(mp.components) do
3✔
111
        new_components[i] = fn(pat, i)
2✔
112
    end
113
    return multipattern.new(new_components)
1✔
114
end
115

116
--- Filter out sub-patterns according to a predicate.
117
-- Keeps only those patterns for which `predicate(pattern) == true`.
118
--
119
-- **Example**:
120
--  ```
121
--  local bigSegs = mp:filter(function(p) return p:size() >= 10 end)
122
--  ```
123
-- @param mp the multipattern upon which to filter.
124
-- @param fn a function `(pattern) -> boolean`.
125
-- @return a new multipattern containing only the sub-patterns passing the test.
126
function multipattern.filter(mp, fn)
1✔
127
    assert(getmetatable(mp) == multipattern, "multipattern.filter requires a multipattern as an argument")
14✔
128
    -- Keeps only those patterns for which fn(pat) == true.
129
    local new_components = {}
14✔
130
    for _, pat in ipairs(mp.components) do
75✔
131
        if fn(pat) then
61✔
132
            new_components[#new_components + 1] = pat
15✔
133
        end
134
    end
135
    return multipattern.new(new_components)
14✔
136
end
137

138
--- Apply a named method to each pattern, returning a new multipattern.
139
-- This is an alternative to `:map(...)` for calling an *existing* pattern method
140
-- by name on all sub-patterns. You may also supply arguments to that method.
141
--
142
-- Note that when used with a method that generates multipatterns (e.g. `connected_components`),
143
-- the results will be 'flattened' into a single multipattern.
144
--
145
-- **Example**:
146
--   ```
147
--   local translated = mp:apply("translate", 10, 5)
148
--   -- calls p:translate(10,5) on each pattern p
149
--   ```
150
-- @param mp the multipattern upon which to apply the method.
151
-- @param method the name of a function in `pattern`.
152
-- @param ... additional arguments to pass to that method.
153
-- @return a new multipattern of the method's results.
154
function multipattern.apply(mp, method, ...)
1✔
155
    assert(getmetatable(mp) == multipattern, "multipattern.apply requires a multipattern as an argument")
2✔
156
    local pattern_mt = require('forma.pattern')
2✔
157
    local new_components = {}
2✔
158
    for _, pat in ipairs(mp.components) do
7✔
159
        local m = pat[method]
5✔
160
        assert(type(m) == "function", "No method named '" .. tostring(method) .. "' on pattern")
5✔
161
        local return_value = m(pat, ...)
5✔
162
        if getmetatable(return_value) == multipattern then
5✔
163
            for _, v in ipairs(return_value.components) do
9✔
164
                table.insert(new_components, v)
6✔
165
            end
166
        elseif getmetatable(return_value) == pattern_mt then
2✔
167
            table.insert(new_components, return_value)
2✔
168
        else
NEW
169
            assert(false, "Method must return a pattern or multipattern")
×
170
        end
171
    end
172
    return multipattern.new(new_components)
2✔
173
end
174

175
--- Union all sub-patterns into a single pattern.
176
-- Folds over the sub-patterns with the union (`+`) operator,
177
-- returning a single `pattern`.
178
--
179
-- **Example**:
180
--   ```
181
--   local combined = mp:union_all()
182
--   ```
183
-- @param mp the multipattern to union over.
184
-- @return a single pattern combining all sub-patterns.
185
function multipattern.union_all(mp)
1✔
186
    -- Require here to avoid circular dependency.
187
    local pattern = require('forma.pattern')
9✔
188
    if mp:n_components() == 0 then
9✔
NEW
189
        return pattern.new()
×
190
    else
191
        return pattern.union(mp.components)
9✔
192
    end
193
end
194

195
--- Utilities
196
-- @section multipattern_utils
197

198
--- Print a multipattern.
199
-- Prints a multipattern to `io.output`. If provided, a table of subpattern labels
200
-- can be used, with one entry per subpattern.
201
-- @param mp the multipattern to be drawn.
202
-- @param chars the characters to be printed for each subpattern (optional).
203
-- @param domain the domain in which to print (optional).
204
function multipattern.print(mp, chars, domain)
1✔
UNCOV
205
    assert(getmetatable(mp) == multipattern, "multipattern.print requires a multipattern as a first argument")
×
206
    domain = domain or mp:union_all()
×
207
    assert(domain:size() > 0, "multipattern.print: domain must have at least one cell")
×
NEW
208
    if domain.bbox_dirty then domain:recalculate_bounding_box() end
×
209
    local n = mp:n_components()
×
210
    -- If no dictionary is supplied generate a new one (starting from '0')
UNCOV
211
    if chars == nil then
×
212
        local start_char = 47
×
213
        assert(n < (200 - start_char), "multipattern.print: too many components")
×
214
        chars = {}
×
215
        for i = 1, n, 1 do
×
216
            table.insert(chars, string.char(i + start_char))
×
217
        end
218
    end
UNCOV
219
    assert(n == #chars,
×
220
        "multipattern.print: there must be as many character table entries as components")
×
221
    -- Print out the segments to a map
UNCOV
222
    for i = domain.min.y, domain.max.y, 1 do
×
NEW
223
        local row = {}
×
224
        for j = domain.min.x, domain.max.x, 1 do
×
225
            local token = ' '
×
226
            for k, v in ipairs(mp.components) do
×
227
                if v:has_cell(j, i) then token = chars[k] end
×
228
            end
NEW
229
            row[#row + 1] = token
×
230
        end
NEW
231
        io.write(table.concat(row) .. '\n')
×
232
    end
233
end
234

235
return multipattern
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc