Sounds like a Dictionary
is not the optimal data structure for your problem.
I created a data structure which you might find useful: the SegmentedLine
. You'll also need the binary search function from here, but otherwise everything is in that one file.
I suggested adding it to swift-collection
because I find it's not entirely uncommon that I need this kind of thing. I use it for Unicode databases - mapValues
followed by combineSegments
is really effective at generating simplified, compacted data.
A SegmentedLine
is a one-dimensional space, where every location is assigned a value.
SegmentedLine
is effective when entire regions are assigned the same value.
For example, we can build a simple number line to tag ranges of integers; in this case,
we're tagging each range with an optional string.
var line = SegmentedLine<Int, String?>(bounds: 0..<100, value: nil)
// After setting values <5 to "small" and values >10 to "large",
// the gap is left with its previous value, "medium".
line.set(0..<20, to: "medium")
line.set(0..<5, to: "small")
line.set(10..<60, to: "large")
print(line)
// | [0..<5]: "small" | [5..<10]: "medium" | [10..<60]: "large" | [60..<100]: nil |
The locations on a SegmentedLine
do not have to be integers - they can be any Comparable
type,
including dates, strings, Unicode scalars (for building character sets), or Collection
indexes.
In the latter case, we can model a Collection's elements as a line from its startIndex
to its endIndex
,
allowing us to annotate regions of any Collection. In a way, it can be used as a generalized AttributedString
.
let string = "Bob is feeling great"
// Create a SegmentedLine for the collection's contents.
// Start by setting a font attribute over the entire string.
var tags = SegmentedLine(
bounds: string.startIndex..<string.endIndex,
value: [Font.custom("Comic Sans")] as [Any]
)
// Set each word to a different color.
// Use 'modify' to append the attribute, but only for the region
// we're modifying.
for word: Substring in string.split(separator: " ") {
tags.modify(word.startIndex..<word.endIndex) { attributes in
attributes.append(Color.random())
}
}
// Check the result.
// - ✅ Every segment still contains the font attribute.
// - ✅ Each word also contains its individual color attribute.
for (range, attributes) in tags.segments {
print(#""\#(string[range])""#, "-", attributes)
}
// "Bob" [Font.custom("Comic Sans"), Color.orange]
// " " [Font.custom("Comic Sans")]
// "is" [Font.custom("Comic Sans"), Color.green]
// " " [Font.custom("Comic Sans")]
// "feeling" [Font.custom("Comic Sans"), Color.pink]
// " " [Font.custom("Comic Sans")]
// "great" [Font.custom("Comic Sans"), Color.yellow]