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

nhartland / forma / 22257118818

21 Feb 2026 12:50PM UTC coverage: 97.413% (-1.0%) from 98.384%
22257118818

Pull #29

github

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

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

28 existing lines in 4 files now uncovered.

2109 of 2165 relevant lines covered (97.41%)

7567.31 hits per line

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

71.11
/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

22
--- Create a new multipattern from a list of patterns.
23
-- @param components an array of `pattern` objects.
24
-- @return a new multipattern containing those patterns.
25
function multipattern.new(components)
1✔
26
    local mp = {
59✔
27
        components = components or {}
59✔
28
    }
29
    mp = setmetatable(mp, multipattern)
59✔
30
    return mp
59✔
31
end
32

33
--- Clone the multipattern.
34
-- @param mp multipattern to clone.
35
-- @return the cloned multipattern.
36
function multipattern.clone(mp)
1✔
37
    local components = {}
1✔
38
    for i, p in ipairs(mp.components) do
1,265✔
39
        components[i] = p:clone()
1,264✔
40
    end
41
    return multipattern.new(components)
1✔
42
end
43

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

67
--- Insert a pattern into the multipattern.
68
-- @param mp multipattern to be operated upon.
69
-- @param ip the new pattern to insert.
70
-- @return the multipattern.
71
function multipattern.insert(mp, ip)
1✔
72
    assert(getmetatable(mp) == multipattern, "multipattern.insert requires a multipattern as the first argument")
1,350✔
73
    table.insert(mp.components, ip)
1,350✔
74
    return mp
1,350✔
75
end
76

77
--- Count the number of components in a multipattern.
78
-- @param mp the multipattern to count.
79
-- @return the number of components.
80
function multipattern.n_components(mp)
1✔
81
    assert(getmetatable(mp) == multipattern, "multipattern.n_components requires a multipattern as the first argument")
36✔
82
    return #mp.components
36✔
83
end
84

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

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

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

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

188
--- Utilities
189
-- @section multipattern_utils
190

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

228
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