Skip to content

Commit 31f5588

Browse files
committed
Added proper currency pattern matching.
1 parent 742d1d5 commit 31f5588

File tree

2 files changed

+104
-41
lines changed

2 files changed

+104
-41
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//=----------------------------------------------------------------------------=
2+
// This source file is part of the DiffableTextViews open source project.
3+
//
4+
// Copyright (c) 2022 Oscar Byström Ericsson
5+
// Licensed under Apache License, Version 2.0
6+
//
7+
// See http://www.apache.org/licenses/LICENSE-2.0 for license information.
8+
//=----------------------------------------------------------------------------=
9+
10+
import DiffableTextKit
11+
12+
//*============================================================================*
13+
// MARK: * Search
14+
//*============================================================================*
15+
16+
/// Naive search is OK for matching currencies.
17+
@usableFromInline enum Search<Haystack, Needle> where
18+
Haystack: BidirectionalCollection, Haystack.Element == Symbol,
19+
Needle: BidirectionalCollection, Needle.Element == Character {
20+
@usableFromInline typealias Location = Range<Haystack.Index>
21+
@usableFromInline typealias Reversed = Search<ReversedCollection<Haystack>, ReversedCollection<Needle>>
22+
23+
//=------------------------------------------------------------------------=
24+
// MARK: Forwards
25+
//=------------------------------------------------------------------------=
26+
27+
/// A naive search, for needles known to be at or near the start of a haystack.
28+
@inlinable static func forwards(search haystack: Haystack, match needle: Needle) -> Location? {
29+
//=--------------------------------------=
30+
// MARK: Haystack
31+
//=--------------------------------------=
32+
for start in haystack.indices {
33+
var position = start; var found = true
34+
//=----------------------------------=
35+
// MARK: Needle
36+
//=----------------------------------=
37+
for character in needle {
38+
guard position != haystack.endIndex,
39+
character == haystack[position].character
40+
else { found = false; break }
41+
haystack.formIndex(after: &position)
42+
}
43+
//=----------------------------------=
44+
// MARK: Success
45+
//=----------------------------------=
46+
if found { return start ..< position }
47+
}
48+
//=--------------------------------------=
49+
// MARK: Failure
50+
//=--------------------------------------=
51+
return nil
52+
}
53+
54+
//=------------------------------------------------------------------------=
55+
// MARK: Backwards
56+
//=------------------------------------------------------------------------=
57+
58+
/// A naive search, for needles known to be at or near the end of a haystack.
59+
@inlinable static func backwards(search haystack: Haystack, match needle: Needle) -> Location? {
60+
Reversed.forwards(search: haystack.reversed(), match: needle.reversed()).map { reversed in
61+
reversed.upperBound.base ..< reversed.lowerBound.base
62+
}
63+
}
64+
65+
//=------------------------------------------------------------------------=
66+
// MARK: Forwards / Backwards
67+
//=------------------------------------------------------------------------=
68+
69+
/// A naive search, for needles known to be at or near the edge of a haystack.
70+
@inlinable static func range(of needle: Needle, in haystack: Haystack, direction: Direction) -> Location? {
71+
switch direction {
72+
case .forwards: return forwards(search: haystack, match: needle)
73+
case .backwards: return backwards(search: haystack, match: needle)
74+
}
75+
}
76+
}

Sources/DiffableTextStylesXNumeric/Models/Scheme+Currency.swift

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import Foundation
4545
//=--------------------------------------=
4646
formatter.numberStyle = .currency
4747
self.preferences = Preferences(formatter)
48-
self.instruction = Instruction(id, lexicon)
48+
self.instruction = Instruction(formatter, lexicon)
4949
}
5050

5151
//=------------------------------------------------------------------------=
@@ -108,68 +108,55 @@ import Foundation
108108
///
109109
/// Characters used to express currencies are usually disjoint
110110
/// from characters used to express amounts, but sometimes they overlap.
111-
/// This instruction is used to efficiently mark faux fraction separators, when they exist.
111+
/// This instruction is used to efficiently mark currency labels when needed.
112112
///
113113
@usableFromInline struct Instruction {
114114

115115
//=--------------------------------------------------------------------=
116116
// MARK: State
117117
//=--------------------------------------------------------------------=
118118

119-
@usableFromInline let occurances: Int
120-
@usableFromInline let character: Character
119+
@usableFromInline let label: String
121120
@usableFromInline let direction: Direction
122121

123122
//=--------------------------------------------------------------------=
124123
// MARK: Initializers
125124
//=--------------------------------------------------------------------=
126125

127-
@inlinable init?<S>(_ label: S, _ character: Character,
128-
_ direction: Direction) where S: Sequence, S.Element == Character {
129-
self.occurances = label.count(where: { $0 == character })
130-
//=--------------------------------------=
131-
// MARK: Validate
132-
//=--------------------------------------=
133-
guard occurances > 0 else { return nil }
134-
//=--------------------------------------=
135-
// MARK: Instantiate
136-
//=--------------------------------------=
137-
self.character = character
138-
self.direction = direction
139-
}
140-
141-
@inlinable init?(_ id: ID, _ lexicon: Lexicon) {
142-
let separator = lexicon.separators[.fraction]
143-
let labels = IntegerFormatStyle<Int>
144-
.Currency(code: id.code, locale: id.locale)
145-
.precision(.fractionLength(0)).format(0).split(
146-
separator: lexicon.digits[.zero],
147-
omittingEmptySubsequences: false)
126+
/// Requires that formatter.numberStyle == .currency
127+
///
128+
/// Correctness is assert by tests parsing currency formats for all locale-currency pairs.
129+
///
130+
@inlinable init?(_ formatter: NumberFormatter, _ lexicon: Lexicon) {
131+
self.label = formatter.currencySymbol
132+
//=----------------------------------=
133+
// MARK: Check Instruction Is Needed
134+
//=----------------------------------=
135+
guard label.contains(lexicon.separators[.fraction]) else { return nil }
136+
//=----------------------------------=
137+
// MARK: Formatted
148138
//=----------------------------------=
149-
// MARK: Instantiate
139+
let sides = IntegerFormatStyle<Int>
140+
.Currency(code: formatter.currencyCode, locale: formatter.locale)
141+
.precision(.fractionLength(0)).format(0)
142+
.split(separator: lexicon.digits[.zero], omittingEmptySubsequences: false)
150143
//=----------------------------------=
151-
if let instance = Self(labels[0], separator, .forwards) { self = instance }
152-
else if let instance = Self(labels[1], separator, .backwards) { self = instance }
153-
else { return nil }
144+
// MARK: Direction
145+
//=----------------------------------=
146+
if sides[0].contains(label) {
147+
self.direction = .forwards
148+
} else {
149+
self.direction = .backwards; assert(sides[1].contains(label))
150+
}
154151
}
155152

156153
//=------------------------------------------------------------------------=
157154
// MARK: Utilities
158155
//=------------------------------------------------------------------------=
159156

160157
@inlinable func autocorrect(_ snapshot: inout Snapshot) {
161-
switch direction {
162-
case .forwards: autocorrect(&snapshot, indices: snapshot.indices)
163-
case .backwards: autocorrect(&snapshot, indices: snapshot.indices.reversed())
164-
}
165-
}
166-
167-
@inlinable func autocorrect<S>(_ snapshot: inout Snapshot,
168-
indices: S) where S: Sequence, S.Element == Snapshot.Index {
169-
var count = 0; for index in indices where
170-
snapshot[index].character == character {
171-
snapshot.update(attributes: index) { $0 = .phantom }
172-
count += 1; guard count < occurances else { return }
158+
if let range = Search.range(of: label, in: snapshot, direction: direction) {
159+
snapshot.update(attributes: range) { attribute in attribute = .phantom }
173160
}
174161
}
175162
}

0 commit comments

Comments
 (0)