Skip to content

Commit ad47eb5

Browse files
committed
docs(equiv checker): illustrate with venn diagrams
1 parent c452745 commit ad47eb5

File tree

1 file changed

+144
-30
lines changed

1 file changed

+144
-30
lines changed

equiv-checker.html

Lines changed: 144 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,38 @@
152152
color: #721c24;
153153
}
154154

155+
.result.subset {
156+
background: #cce5ff;
157+
border: 1px solid #99ccff;
158+
color: #003d7a;
159+
}
160+
161+
.result.superset {
162+
background: #e5ccff;
163+
border: 1px solid #cc99ff;
164+
color: #4d0080;
165+
}
166+
167+
.venn-container {
168+
display: flex;
169+
justify-content: center;
170+
margin: 20px 0;
171+
}
172+
173+
.venn-diagram {
174+
max-width: 300px;
175+
width: 100%;
176+
}
177+
178+
.result code {
179+
background: #e9ecef;
180+
padding: 2px 6px;
181+
border-radius: 3px;
182+
font-family: 'Courier New', monospace;
183+
margin: 2px;
184+
display: inline-block;
185+
}
186+
155187
.result.error {
156188
background: #fff3cd;
157189
border: 1px solid #ffeaa7;
@@ -211,7 +243,7 @@ <h1>RegExp Equivalence Checker</h1>
211243

212244
<div class="input-container">
213245
<div class="input-group">
214-
<label for="regex1">Regular Expression 1:</label>
246+
<label for="regex1">RegExp 1:</label>
215247
<div class="regex-input-wrapper">
216248
<span class="regex-slash">/</span>
217249
<input type="text" id="regex1" placeholder="^a{1,3}$" />
@@ -221,7 +253,7 @@ <h1>RegExp Equivalence Checker</h1>
221253
</div>
222254

223255
<div class="input-group">
224-
<label for="regex2">Regular Expression 2:</label>
256+
<label for="regex2">RegExp 2:</label>
225257
<div class="regex-input-wrapper">
226258
<span class="regex-slash">/</span>
227259
<input type="text" id="regex2" placeholder="^(a|aa|aaa)$" />
@@ -235,13 +267,35 @@ <h1>RegExp Equivalence Checker</h1>
235267
<button id="check-equiv-button">Check Equivalence</button>
236268
</div>
237269

238-
<div class="result" id="result"></div>
270+
<svg viewBox="0 0 300 200" class="venn-diagram venn-diagram-superset" style="display: none;">
271+
<circle cx="100" cy="100" r="60" stroke="#667eea" stroke-width="2" fill-opacity="0.3" fill="#e9ecef"></circle>
272+
<circle cy="100" stroke="#764ba2" stroke-width="2" fill-opacity="0.3" fill="#764ba2" cx="120" r="30"></circle>
273+
<text x="70" y="80" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#667eea">1</text>
274+
<text font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#764ba2" x="115" y="105">2</text>
275+
</svg>
276+
277+
<svg viewBox="0 0 300 200" class="venn-diagram venn-diagram-subset" style="display: none;">
278+
<circle cx="80" cy="100" r="30" stroke="#667eea" stroke-width="2" fill-opacity="0.3" fill="#667eea"></circle>
279+
<circle cx="100" cy="100" stroke="#764ba2" stroke-width="2" fill-opacity="0.3" fill="#e9ecef" r="60"></circle>
280+
<text x="75" y="105" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#667eea">1</text>
281+
<text font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#764ba2" x="120" y="80">2</text>
282+
</svg>
283+
284+
<svg viewBox="0 0 300 200" class="venn-diagram venn-diagram-intersection" style="display: none;">
285+
<circle cx="80" cy="100" r="60" stroke="#667eea" stroke-width="2" fill-opacity="0.3" fill="#667eea"></circle>
286+
<circle cx="140" cy="100" stroke="#764ba2" stroke-width="2" fill-opacity="0.3" fill="#764ba2" r="60"></circle>
287+
<text x="50" y="105" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#667eea">1</text>
288+
<text font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#764ba2" x="150" y="80">2</text>
289+
</svg>
290+
291+
<div class="result" id="result">
292+
293+
</div>
239294

240295
<div class="info">
241296
<h4>How to use:</h4>
242297
Enter two JavaScript regular expressions (without the surrounding slashes) and click "Check Equivalence".
243-
The tool will determine if both expressions match exactly the same set of strings.
244-
If they're not equivalent, it will show example strings that demonstrate the difference.
298+
The tool will determine the relationship between the two expressions: equivalent, subset, superset, or neither.
245299

246300
<h4>Examples:</h4>
247301
<ul>
@@ -329,32 +383,35 @@ <h4>Powered by:</h4>
329383
const regexA = new RegExp(pattern1);
330384
const regexB = new RegExp(pattern2);
331385

332-
// Then compute symmetric difference of the two regex:
333-
const diffAB = RB(regexA).without(regexB);
334-
const diffBA = RB(regexB).without(regexA);
335-
336-
if (diffAB.isEmpty() && diffBA.isEmpty()) {
337-
// Equivalent
338-
showResult('✅ The regular expressions are equivalent!<br>Both expressions match exactly the same set of strings.', 'equivalent');
386+
// Compute differences between the two regex
387+
const diffAB = RB(regexA).without(regexB); // strings in A but not B
388+
const diffBA = RB(regexB).without(regexA); // strings in B but not A
389+
390+
const diffABEmpty = diffAB.isEmpty();
391+
const diffBAEmpty = diffBA.isEmpty();
392+
393+
if (diffABEmpty && diffBAEmpty) {
394+
// Equivalent: both differences are empty
395+
showResultWithDiagram('✅ The regular expressions are equivalent! Both expressions match exactly the same strings.', 'equivalent');
396+
} else if (diffABEmpty && !diffBAEmpty) {
397+
// A is subset of B: no strings in A that aren't in B
398+
const stringsMatchingBButNotA = Array.from(diffBA.enumerate().take(5));
399+
const examples = stringsMatchingBButNotA.length > 0 ? { regex2Only: stringsMatchingBButNotA } : null;
400+
showResultWithDiagram('📊 RegExp 1 matches a subset of RegExp 2. Every string matched by RegExp 1 is also matched by RegExp 2, but not vice versa.', 'subset', examples);
401+
} else if (!diffABEmpty && diffBAEmpty) {
402+
// B is subset of A: no strings in B that aren't in A
403+
const stringsMatchingAButNotB = Array.from(diffAB.enumerate().take(5));
404+
const examples = stringsMatchingAButNotB.length > 0 ? { regex1Only: stringsMatchingAButNotB } : null;
405+
showResultWithDiagram('📊 RegExp 1 matches a superset of RegExp 2. Every string matched by RegExp 2 is also matched by RegExp 1, but not vice versa.', 'superset', examples);
339406
} else {
340-
// Not equivalent - show counterexamples under respective text boxes
341-
showResult('❌ The regular expressions are NOT equivalent.', 'not-equivalent');
342-
343-
// Show counterexamples under first regex (strings matching first but not second)
344-
if (!diffAB.isEmpty()) {
345-
const stringsMatchingAButNotB = Array.from(diffAB.enumerate().take(5));
346-
if (stringsMatchingAButNotB.length > 0) {
347-
showCounterexamples(counterexamples1, 'Matches only left regex:', stringsMatchingAButNotB);
348-
}
349-
}
350-
351-
// Show counterexamples under second regex (strings matching second but not first)
352-
if (!diffBA.isEmpty()) {
353-
const stringsMatchingBButNotA = Array.from(diffBA.enumerate().take(5));
354-
if (stringsMatchingBButNotA.length > 0) {
355-
showCounterexamples(counterexamples2, 'Matches only right regex:', stringsMatchingBButNotA);
356-
}
357-
}
407+
// Neither subset nor equivalent: both differences are non-empty
408+
const stringsMatchingAButNotB = Array.from(diffAB.enumerate().take(5));
409+
const stringsMatchingBButNotA = Array.from(diffBA.enumerate().take(5));
410+
const examples = {
411+
regex1Only: stringsMatchingAButNotB.length > 0 ? stringsMatchingAButNotB : null,
412+
regex2Only: stringsMatchingBButNotA.length > 0 ? stringsMatchingBButNotA : null
413+
};
414+
showResultWithDiagram('❌ The two RegExp are not equivalent. Neither matches a subset of the other.', 'not-equivalent', examples);
358415
}
359416
} catch (error) {
360417
if (error instanceof SyntaxError) {
@@ -390,6 +447,62 @@ <h4>Powered by:</h4>
390447
resultDiv.style.display = 'block';
391448
}
392449

450+
function showResultWithDiagram(message, type, examples = null) {
451+
const resultDiv = document.getElementById('result');
452+
453+
// Build the complete HTML structure
454+
let html = message;
455+
456+
// Add Venn diagram (only for subset/superset/not-equivalent cases)
457+
if (type !== 'equivalent') {
458+
html += '<div class="venn-container"></div>';
459+
}
460+
461+
// Add examples if provided
462+
if (examples) {
463+
html += '<p>For example, '
464+
465+
if (examples.regex1Only && examples.regex1Only.length > 0) {
466+
html += `RegExp 1 matches `;
467+
html += examples.regex1Only.map(str => `<code>${JSON.stringify(str)}</code>`).join(', ');
468+
html += ' but RegExp 2 does not. ';
469+
}
470+
471+
if (examples.regex2Only && examples.regex2Only.length > 0) {
472+
html += `RegExp 2 matches `;
473+
html += examples.regex2Only.map(str => `<code>${JSON.stringify(str)}</code>`).join(', ');
474+
html += 'but RegExp 1 does not. ';
475+
}
476+
477+
html += '</p>'
478+
}
479+
480+
resultDiv.innerHTML = html;
481+
482+
// Show/hide the appropriate hardcoded SVG
483+
if (type !== 'equivalent') {
484+
const vennContainer = resultDiv.querySelector('.venn-container');
485+
let targetDiagram;
486+
487+
if (type === 'subset') {
488+
targetDiagram = document.querySelector('.venn-diagram-subset');
489+
} else if (type === 'superset') {
490+
targetDiagram = document.querySelector('.venn-diagram-superset');
491+
} else if (type === 'not-equivalent') {
492+
targetDiagram = document.querySelector('.venn-diagram-intersection');
493+
}
494+
495+
if (targetDiagram) {
496+
const clonedDiagram = targetDiagram.cloneNode(true);
497+
clonedDiagram.style.display = 'block';
498+
vennContainer.appendChild(clonedDiagram);
499+
}
500+
}
501+
502+
resultDiv.className = `result ${type}`;
503+
resultDiv.style.display = 'block';
504+
}
505+
393506
function showCounterexamples(container, title, examples) {
394507
let html = '<div class="counterexamples">';
395508
html += `<h4>${title}</h4>`;
@@ -409,6 +522,7 @@ <h4>Powered by:</h4>
409522
div.textContent = text;
410523
return div.innerHTML;
411524
}
525+
412526
})
413527
</script>
414528
</body>

0 commit comments

Comments
 (0)