normalized_match 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -1
- data/lib/normalized_match/normalized_match_matcher.rb +150 -151
- data/lib/normalized_match.rb +0 -7
- data/normalized_match.gemspec +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c9e104e74805c7efb7da5fcc8c15f18ee585d5c9b426eeef35273ba9c90a089
|
4
|
+
data.tar.gz: f124fb69c3f63b57d73e3c8517baad43e473cb67db12e831cda4cfc73efa6e00
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 56d6051c176e5aad379b460d631c6aa83dd4831ccea7dbbac4ea34787dc1a749e9932ba2559d33299eb532795cbdebcf384a9bb42ea2d6d8f094c6a53f44bf55
|
7
|
+
data.tar.gz: ae3cc9960db2e4c515d47843773177412dd9896589e6305ed2c9fe504bc3634e7c887f64c1f13b2f5f5b0f311606d814e5378de3782114ae658a32d9844c9a2e
|
data/README.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# Normalized Match
|
2
2
|
|
3
|
+
[](https://github.com/firstdraft/normalized_match/actions/workflows/ci.yml)
|
4
|
+
[](https://badge.fury.io/rb/normalized_match)
|
5
|
+
[](https://github.com/standardrb/standard)
|
6
|
+
|
3
7
|
A normalized string matcher for RSpec that ignores case, punctuation, and some whitespace differences.
|
4
8
|
|
5
9
|
## Installation
|
@@ -96,7 +100,7 @@ d: 1
|
|
96
100
|
|
97
101
|
## Example Output
|
98
102
|
|
99
|
-
When strings don't match after normalization:
|
103
|
+
When strings don't match after normalization, the matcher displays a helpful table:
|
100
104
|
|
101
105
|
```
|
102
106
|
Normalized match failed!
|
@@ -6,185 +6,184 @@ module NormalizedMatch
|
|
6
6
|
COLUMN_PADDING = 4 # 2 spaces on each side
|
7
7
|
LABEL_PADDING = 2 # For borders in label column
|
8
8
|
RSpec::Matchers.define :normalized_match do |expected|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
match do |actual|
|
10
|
+
# Store the original values
|
11
|
+
@original_expected = expected
|
12
|
+
@original_actual = actual
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
# Store normalized versions for cleaner diffs
|
15
|
+
@expected = normalize_string(expected)
|
16
|
+
@actual = normalize_string(actual)
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
failure_message do |actual|
|
23
|
-
# Split into lines for side-by-side comparison
|
24
|
-
orig_expected_lines = @original_expected.to_s.split("\n")
|
25
|
-
orig_actual_lines = @original_actual.to_s.split("\n")
|
26
|
-
norm_expected_lines = @expected.to_s.split("\n")
|
27
|
-
norm_actual_lines = @actual.to_s.split("\n")
|
28
|
-
|
29
|
-
# Check if normalization changed anything
|
30
|
-
expected_changed = @original_expected.to_s != @expected.to_s
|
31
|
-
actual_changed = @original_actual.to_s != @actual.to_s
|
32
|
-
normalization_needed = expected_changed || actual_changed
|
33
|
-
|
34
|
-
# Calculate column widths
|
35
|
-
max_orig_expected = orig_expected_lines.map(&:length).max || 0
|
36
|
-
max_orig_actual = orig_actual_lines.map(&:length).max || 0
|
37
|
-
max_norm_expected = norm_expected_lines.map(&:length).max || 0
|
38
|
-
max_norm_actual = norm_actual_lines.map(&:length).max || 0
|
39
|
-
|
40
|
-
# Ensure minimum column width for headers
|
41
|
-
expected_col_width = [max_orig_expected, max_norm_expected, "EXPECTED".length].max + COLUMN_PADDING
|
42
|
-
actual_col_width = [max_orig_actual, max_norm_actual, "ACTUAL".length].max + COLUMN_PADDING
|
43
|
-
|
44
|
-
# Calculate label column width (for "NORMALIZED" and "ORIGINAL")
|
45
|
-
label_col_width = ["NORMALIZED".length, "ORIGINAL".length].max + COLUMN_PADDING
|
46
|
-
|
47
|
-
# Build the side-by-side table components
|
48
|
-
|
49
|
-
# Helper to pad and center text
|
50
|
-
def center_pad(text, width)
|
51
|
-
text = text.to_s
|
52
|
-
padding = width - text.length
|
53
|
-
left_pad = padding / 2
|
54
|
-
right_pad = padding - left_pad
|
55
|
-
" " * left_pad + text + " " * right_pad
|
56
|
-
end
|
18
|
+
# Check if actual contains expected (substring match)
|
19
|
+
@actual.include?(@expected)
|
20
|
+
end
|
57
21
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
22
|
+
failure_message do |actual|
|
23
|
+
# Split into lines for side-by-side comparison
|
24
|
+
orig_expected_lines = @original_expected.to_s.split("\n")
|
25
|
+
orig_actual_lines = @original_actual.to_s.split("\n")
|
26
|
+
norm_expected_lines = @expected.to_s.split("\n")
|
27
|
+
norm_actual_lines = @actual.to_s.split("\n")
|
28
|
+
|
29
|
+
# Check if normalization changed anything
|
30
|
+
expected_changed = @original_expected.to_s != @expected.to_s
|
31
|
+
actual_changed = @original_actual.to_s != @actual.to_s
|
32
|
+
normalization_needed = expected_changed || actual_changed
|
33
|
+
|
34
|
+
# Calculate column widths
|
35
|
+
max_orig_expected = orig_expected_lines.map(&:length).max || 0
|
36
|
+
max_orig_actual = orig_actual_lines.map(&:length).max || 0
|
37
|
+
max_norm_expected = norm_expected_lines.map(&:length).max || 0
|
38
|
+
max_norm_actual = norm_actual_lines.map(&:length).max || 0
|
39
|
+
|
40
|
+
# Ensure minimum column width for headers
|
41
|
+
expected_col_width = [max_orig_expected, max_norm_expected, "EXPECTED".length].max + COLUMN_PADDING
|
42
|
+
actual_col_width = [max_orig_actual, max_norm_actual, "ACTUAL".length].max + COLUMN_PADDING
|
43
|
+
|
44
|
+
# Calculate label column width (for "NORMALIZED" and "ORIGINAL")
|
45
|
+
label_col_width = ["NORMALIZED".length, "ORIGINAL".length].max + COLUMN_PADDING
|
46
|
+
|
47
|
+
# Build the side-by-side table components
|
48
|
+
|
49
|
+
# Helper to pad and center text
|
50
|
+
def center_pad(text, width)
|
51
|
+
text = text.to_s
|
52
|
+
padding = width - text.length
|
53
|
+
left_pad = padding / 2
|
54
|
+
right_pad = padding - left_pad
|
55
|
+
" " * left_pad + text + " " * right_pad
|
56
|
+
end
|
62
57
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
if normalization_needed
|
69
|
-
header_output << "To make it easier to match the expected output,"
|
70
|
-
header_output << "we are \"normalizing\" both the actual output and"
|
71
|
-
header_output << "expected output in this test. That means we"
|
72
|
-
header_output << "lowercased, removed punctuation, and compacted"
|
73
|
-
header_output << "whitespace in both."
|
74
|
-
header_output << ""
|
75
|
-
header_output << "But the actual output still doesn't contain the"
|
76
|
-
header_output << "expected output. Can you spot the difference?"
|
77
|
-
header_output << ""
|
78
|
-
else
|
79
|
-
header_output << "The actual output doesn't contain the expected"
|
80
|
-
header_output << "output. Can you spot the difference?"
|
81
|
-
header_output << ""
|
82
|
-
end
|
58
|
+
# Helper to left-pad text with 2 spaces on each side
|
59
|
+
def pad_line(text, width)
|
60
|
+
" #{text.to_s.ljust(width - COLUMN_PADDING)} "
|
61
|
+
end
|
83
62
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
63
|
+
# Build header output
|
64
|
+
header_output = []
|
65
|
+
header_output << "Normalized match failed!"
|
66
|
+
header_output << ""
|
67
|
+
|
68
|
+
if normalization_needed
|
69
|
+
header_output << "To make it easier to match the expected output,"
|
70
|
+
header_output << "we are \"normalizing\" both the actual output and"
|
71
|
+
header_output << "expected output in this test. That means we"
|
72
|
+
header_output << "lowercased, removed punctuation, and compacted"
|
73
|
+
header_output << "whitespace in both."
|
74
|
+
header_output << ""
|
75
|
+
header_output << "But the actual output still doesn't contain the"
|
76
|
+
header_output << "expected output. Can you spot the difference?"
|
90
77
|
else
|
91
|
-
|
78
|
+
header_output << "The actual output doesn't contain the expected"
|
79
|
+
header_output << "output. Can you spot the difference?"
|
80
|
+
end
|
81
|
+
header_output << ""
|
82
|
+
|
83
|
+
# Helper to center label text (NORMALIZED/ORIGINAL) in first column
|
84
|
+
def center_label(label, width, total_rows)
|
85
|
+
middle_row = total_rows / 2
|
86
|
+
(0...total_rows).map do |i|
|
87
|
+
if i == middle_row
|
88
|
+
label.center(width)
|
89
|
+
else
|
90
|
+
" " * width
|
91
|
+
end
|
92
|
+
end
|
92
93
|
end
|
93
|
-
end
|
94
|
-
end
|
95
94
|
|
96
|
-
|
97
|
-
|
95
|
+
# Build the table
|
96
|
+
table_output = []
|
98
97
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
98
|
+
if normalization_needed
|
99
|
+
# Build two-section table with labels
|
100
|
+
# Top border
|
101
|
+
table_output << "╔" + "═" * label_col_width + "╦" + "═" * expected_col_width + "╦" + "═" * actual_col_width + "╗"
|
103
102
|
|
104
|
-
|
105
|
-
|
103
|
+
# Header row
|
104
|
+
table_output << "║" + " " * label_col_width + "║" + center_pad("EXPECTED", expected_col_width) + "║" + center_pad("ACTUAL", actual_col_width) + "║"
|
106
105
|
|
107
|
-
|
108
|
-
|
106
|
+
# Header separator
|
107
|
+
table_output << "╠" + "═" * label_col_width + "╬" + "═" * expected_col_width + "╬" + "═" * actual_col_width + "╣"
|
109
108
|
|
110
|
-
|
111
|
-
|
112
|
-
|
109
|
+
# NORMALIZED section
|
110
|
+
max_norm_lines = [norm_expected_lines.length, norm_actual_lines.length].max
|
111
|
+
normalized_labels = center_label("NORMALIZED", label_col_width - LABEL_PADDING, max_norm_lines)
|
113
112
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
113
|
+
max_norm_lines.times do |i|
|
114
|
+
expected_line = norm_expected_lines[i] || ""
|
115
|
+
actual_line = norm_actual_lines[i] || ""
|
116
|
+
label = normalized_labels[i]
|
117
|
+
table_output << "║ #{label} ║" + pad_line(expected_line, expected_col_width) + "║" + pad_line(actual_line, actual_col_width) + "║"
|
118
|
+
end
|
120
119
|
|
121
|
-
|
122
|
-
|
120
|
+
# Middle separator
|
121
|
+
table_output << "╠" + "═" * label_col_width + "╬" + "═" * expected_col_width + "╬" + "═" * actual_col_width + "╣"
|
123
122
|
|
124
|
-
|
125
|
-
|
126
|
-
|
123
|
+
# ORIGINAL section
|
124
|
+
max_lines = [orig_expected_lines.length, orig_actual_lines.length].max
|
125
|
+
original_labels = center_label("ORIGINAL", label_col_width - LABEL_PADDING, max_lines)
|
127
126
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
127
|
+
max_lines.times do |i|
|
128
|
+
expected_line = orig_expected_lines[i] || ""
|
129
|
+
actual_line = orig_actual_lines[i] || ""
|
130
|
+
label = original_labels[i]
|
131
|
+
table_output << "║ #{label} ║" + pad_line(expected_line, expected_col_width) + "║" + pad_line(actual_line, actual_col_width) + "║"
|
132
|
+
end
|
134
133
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
134
|
+
# Bottom border
|
135
|
+
table_output << "╚" + "═" * label_col_width + "╩" + "═" * expected_col_width + "╩" + "═" * actual_col_width + "╝"
|
136
|
+
else
|
137
|
+
# Build simple table without labels column
|
138
|
+
# Top border
|
139
|
+
table_output << "╔" + "═" * expected_col_width + "╦" + "═" * actual_col_width + "╗"
|
141
140
|
|
142
|
-
|
143
|
-
|
141
|
+
# Header row
|
142
|
+
table_output << "║" + center_pad("EXPECTED", expected_col_width) + "║" + center_pad("ACTUAL", actual_col_width) + "║"
|
144
143
|
|
145
|
-
|
146
|
-
|
144
|
+
# Header separator
|
145
|
+
table_output << "╠" + "═" * expected_col_width + "╬" + "═" * actual_col_width + "╣"
|
147
146
|
|
148
|
-
|
149
|
-
|
147
|
+
# Data rows (using original since no normalization happened)
|
148
|
+
max_lines = [orig_expected_lines.length, orig_actual_lines.length].max
|
150
149
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
150
|
+
max_lines.times do |i|
|
151
|
+
expected_line = orig_expected_lines[i] || ""
|
152
|
+
actual_line = orig_actual_lines[i] || ""
|
153
|
+
table_output << "║" + pad_line(expected_line, expected_col_width) + "║" + pad_line(actual_line, actual_col_width) + "║"
|
154
|
+
end
|
156
155
|
|
157
|
-
|
158
|
-
|
159
|
-
|
156
|
+
# Bottom border
|
157
|
+
table_output << "╚" + "═" * expected_col_width + "╩" + "═" * actual_col_width + "╝"
|
158
|
+
end
|
160
159
|
|
161
|
-
|
162
|
-
|
163
|
-
|
160
|
+
# Combine all parts - header, then table (with extra newline between)
|
161
|
+
# Add a trailing newline so there's a blank line before RSpec's diff (when it appears)
|
162
|
+
message = header_output.join("\n") + "\n" + table_output.join("\n") + "\n"
|
164
163
|
|
165
|
-
|
166
|
-
|
164
|
+
message
|
165
|
+
end
|
167
166
|
|
168
|
-
|
169
|
-
|
167
|
+
# These methods make the matcher work better with rspec-enriched_json
|
168
|
+
attr_reader :expected, :actual
|
170
169
|
|
171
|
-
|
172
|
-
|
170
|
+
# Provide access to original values if needed
|
171
|
+
attr_reader :original_expected, :original_actual
|
173
172
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
173
|
+
# Tell RSpec that expected and actual can be diffed
|
174
|
+
def diffable?
|
175
|
+
true
|
176
|
+
end
|
178
177
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
178
|
+
def normalize_string(str)
|
179
|
+
str.to_s
|
180
|
+
.gsub(/[^a-zA-Z0-9\s]/, "") # Remove punctuation but keep all whitespace
|
181
|
+
.downcase
|
182
|
+
.split("\n") # Split by newlines to preserve them
|
183
|
+
.map { |line| line.gsub(/\s+/, " ").strip } # Normalize spaces within each line
|
184
|
+
.reject(&:empty?) # Remove empty lines
|
185
|
+
.join("\n") # Rejoin with newlines
|
186
|
+
end
|
188
187
|
end
|
189
188
|
end
|
190
189
|
end
|
data/lib/normalized_match.rb
CHANGED
@@ -6,10 +6,3 @@ require_relative "normalized_match/normalized_match_matcher"
|
|
6
6
|
# Main namespace.
|
7
7
|
module NormalizedMatch
|
8
8
|
end
|
9
|
-
|
10
|
-
# Include the matcher in RSpec when this gem is loaded
|
11
|
-
if defined?(RSpec)
|
12
|
-
RSpec.configure do |config|
|
13
|
-
config.include NormalizedMatch::NormalizedMatchMatcher
|
14
|
-
end
|
15
|
-
end
|
data/normalized_match.gemspec
CHANGED