@pipeworx/mcp-edge-case-museum 0.1.1 → 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.
- package/README.md +17 -12
- package/bin/cli.js +55 -11
- package/lib/index.js +333 -0
- package/package.json +17 -16
- package/server.json +1 -1
- package/tools.json +98 -0
- package/src/bake.mjs +0 -112
- package/src/ecm-index-data.ts +0 -15
- package/src/exhibits/datetime/add-24-hours-vs-add-1-day-across-dst.json +0 -51
- package/src/exhibits/datetime/add-365-days-to-feb-29-not-next-feb-29.json +0 -49
- package/src/exhibits/datetime/age-calculator-feb29-birthday-crash-vs-silent.json +0 -52
- package/src/exhibits/datetime/century-leap-year-1900-not-leap.json +0 -68
- package/src/exhibits/datetime/datetime-utcnow-naive-deprecated.json +0 -39
- package/src/exhibits/datetime/dst-fall-back-ambiguous-fold.json +0 -51
- package/src/exhibits/datetime/dst-spring-forward-nonexistent-time.json +0 -57
- package/src/exhibits/datetime/fractional-utc-offsets-kathmandu-kolkata.json +0 -50
- package/src/exhibits/datetime/iso-week-year-vs-calendar-year.json +0 -48
- package/src/exhibits/datetime/js-date-field-overflow-rollover.json +0 -49
- package/src/exhibits/datetime/leap-second-rejected-vs-silently-invalid.json +0 -57
- package/src/exhibits/datetime/lord-howe-30-minute-dst.json +0 -47
- package/src/exhibits/datetime/midnight-that-doesnt-exist-sao-paulo.json +0 -53
- package/src/exhibits/datetime/python-naive-aware-comparison-typeerror.json +0 -46
- package/src/exhibits/datetime/samoa-skipped-day-2011.json +0 -50
- package/src/exhibits/datetime/southern-hemisphere-dst-reversed.json +0 -50
- package/src/exhibits/datetime/two-digit-year-century-pivot-mismatch.json +0 -49
- package/src/exhibits/datetime/year-2038-int32-overflow.json +0 -52
- package/src/exhibits/filenames/absolute-second-argument-resets-join.json +0 -62
- package/src/exhibits/filenames/bidi-override-extension-spoof.json +0 -58
- package/src/exhibits/filenames/case-insensitive-filesystem-pure-path-mismatch.json +0 -74
- package/src/exhibits/filenames/dotfile-has-no-extension.json +0 -61
- package/src/exhibits/filenames/extension-trap-double-extension.json +0 -62
- package/src/exhibits/filenames/filename-length-limits-bytes-not-chars.json +0 -66
- package/src/exhibits/filenames/leading-dash-filename-parsed-as-flag.json +0 -67
- package/src/exhibits/filenames/nfc-nfd-filename-lookup-mismatch.json +0 -68
- package/src/exhibits/filenames/ntfs-alternate-data-stream-colon.json +0 -59
- package/src/exhibits/filenames/ntpath-splitdrive-unc-paths.json +0 -58
- package/src/exhibits/filenames/nul-byte-in-filename-raises-at-open.json +0 -60
- package/src/exhibits/filenames/path-join-escapes-the-base-directory.json +0 -63
- package/src/exhibits/filenames/windows-backslash-traversal-ntpath.json +0 -62
- package/src/exhibits/filenames/windows-reserved-device-names.json +0 -73
- package/src/exhibits/filenames/windows-trailing-dot-space-stripped.json +0 -77
- package/src/exhibits/float-money/decimal-from-float-vs-string.json +0 -66
- package/src/exhibits/float-money/float-anchor-0-1-plus-0-2.json +0 -67
- package/src/exhibits/float-money/intl-number-format-de-de-roundtrip-misparsed.json +0 -62
- package/src/exhibits/float-money/iso4217-minor-units-not-always-two.json +0 -78
- package/src/exhibits/float-money/math-abs-negative-zero.json +0 -51
- package/src/exhibits/float-money/max-safe-integer-plus-two.json +0 -62
- package/src/exhibits/float-money/naive-loop-sum-order-dependence.json +0 -71
- package/src/exhibits/float-money/nan-breaks-equality-and-max-min.json +0 -70
- package/src/exhibits/float-money/negative-zero-balance-formatting.json +0 -74
- package/src/exhibits/float-money/number-epsilon-false-positive-small-magnitude.json +0 -52
- package/src/exhibits/float-money/parsefloat-comma-thousands-separator.json +0 -66
- package/src/exhibits/float-money/python-banker-rounding-vs-js-round-half-up.json +0 -65
- package/src/exhibits/float-money/python-round-2-675-float-imprecision.json +0 -64
- package/src/exhibits/float-money/splitting-100-dollars-three-ways-leaves-a-cent.json +0 -63
- package/src/exhibits/float-money/sum-ten-dimes-not-a-dollar.json +0 -77
- package/src/exhibits/float-money/tofixed-1-005-rounds-down-not-up.json +0 -63
- package/src/exhibits/float-money/tofixed-2-345-inconsistent-rounding.json +0 -64
- package/src/exhibits/integers/array-index-above-2-32-2-not-an-index.json +0 -42
- package/src/exhibits/integers/bigint-number-mixing-typeerror.json +0 -49
- package/src/exhibits/integers/bitwise-32-bit-truncation.json +0 -56
- package/src/exhibits/integers/bool-is-int-subclass-dict-key-collision.json +0 -58
- package/src/exhibits/integers/floor-div-mod-sign-python-vs-js.json +0 -48
- package/src/exhibits/integers/json-parse-loses-id-precision.json +0 -58
- package/src/exhibits/integers/max-safe-integer-plus-one-equals-plus-two.json +0 -58
- package/src/exhibits/integers/number-empty-and-whitespace-string-equals-zero.json +0 -55
- package/src/exhibits/integers/parseint-null-radix36-digit-soup.json +0 -57
- package/src/exhibits/integers/parseint-number-argument-scientific-notation.json +0 -48
- package/src/exhibits/integers/parseint-stops-at-first-invalid-char-vs-number.json +0 -58
- package/src/exhibits/integers/python-int-accepts-fullwidth-unicode-digits.json +0 -56
- package/src/exhibits/integers/python-int-accepts-underscores-and-whitespace.json +0 -48
- package/src/exhibits/integers/python-unbounded-int-vs-fixed-width-wrap.json +0 -49
- package/src/exhibits/names-addresses/address-with-no-street-number.json +0 -54
- package/src/exhibits/names-addresses/ascii-regex-rejects-accented-and-hyphenated-names.json +0 -55
- package/src/exhibits/names-addresses/house-number-suffix-collapsed-by-digit-extraction.json +0 -55
- package/src/exhibits/names-addresses/mononym-fails-first-last-required.json +0 -56
- package/src/exhibits/names-addresses/naive-lowercase-breaks-greek-final-sigma.json +0 -64
- package/src/exhibits/names-addresses/null-surname-coerced-to-null-value.json +0 -58
- package/src/exhibits/names-addresses/single-letter-surname-fails-min-length.json +0 -55
- package/src/exhibits/names-addresses/sort-by-surname-particle-convention-mismatch.json +0 -57
- package/src/exhibits/names-addresses/spanish-double-surname-treated-as-single.json +0 -55
- package/src/exhibits/names-addresses/split-surname-on-last-space-loses-particle.json +0 -59
- package/src/exhibits/names-addresses/title-case-mangles-mixed-case-and-apostrophes.json +0 -60
- package/src/exhibits/names-addresses/uk-canada-postal-code-assumed-numeric.json +0 -55
- package/src/exhibits/names-addresses/us-zip-code-leading-zero-lost-as-integer.json +0 -58
- package/src/exhibits/names-addresses/varchar-truncation-splits-name-mid-grapheme.json +0 -63
- package/src/exhibits/parsing/csv-crlf-trailing-blank-line.json +0 -87
- package/src/exhibits/parsing/csv-doubled-quote-escaping.json +0 -70
- package/src/exhibits/parsing/csv-formula-injection-excel.json +0 -71
- package/src/exhibits/parsing/csv-quoted-field-with-comma.json +0 -70
- package/src/exhibits/parsing/csv-quoted-field-with-newline.json +0 -70
- package/src/exhibits/parsing/csv-semicolon-delimited-european-excel.json +0 -70
- package/src/exhibits/parsing/csv-utf8-bom-first-header.json +0 -74
- package/src/exhibits/parsing/email-regex-rejects-valid-apostrophe-address.json +0 -68
- package/src/exhibits/parsing/json-big-integer-precision-loss.json +0 -65
- package/src/exhibits/parsing/json-duplicate-keys-last-wins.json +0 -64
- package/src/exhibits/parsing/json-lone-surrogate-roundtrip.json +0 -58
- package/src/exhibits/parsing/json-nan-infinity-not-valid-json.json +0 -54
- package/src/exhibits/parsing/url-backslash-treated-as-slash-whatwg.json +0 -60
- package/src/exhibits/parsing/url-urlparse-never-raises-on-garbage.json +0 -60
- package/src/exhibits/parsing/url-userinfo-trusted-looking-host.json +0 -60
- package/src/exhibits/sorting/accented-letters-sort-after-z.json +0 -63
- package/src/exhibits/sorting/byte-order-caps-before-lowercase.json +0 -66
- package/src/exhibits/sorting/case-insensitive-sort-lower-vs-casefold.json +0 -56
- package/src/exhibits/sorting/comparator-returns-boolean-js.json +0 -49
- package/src/exhibits/sorting/js-array-sort-stringifies-numbers.json +0 -68
- package/src/exhibits/sorting/locale-collation-de-vs-sv.json +0 -54
- package/src/exhibits/sorting/localecompare-numeric-option.json +0 -50
- package/src/exhibits/sorting/nan-in-sort.json +0 -59
- package/src/exhibits/sorting/natural-sort-breaks-lexicographic.json +0 -63
- package/src/exhibits/sorting/postgres-collation-documented.json +0 -61
- package/src/exhibits/sorting/python-cannot-compare-str-int.json +0 -70
- package/src/exhibits/sorting/reverse-sort-vs-reverse-after-sort-ties.json +0 -48
- package/src/exhibits/sorting/sorting-mixed-none-null.json +0 -56
- package/src/exhibits/sorting/typed-array-sort-is-numeric-by-default.json +0 -48
- package/src/exhibits/unicode/arabic-indic-digit-parses.json +0 -73
- package/src/exhibits/unicode/bidi-override-filename-spoof.json +0 -62
- package/src/exhibits/unicode/cyrillic-homoglyph-paypal-spoof.json +0 -67
- package/src/exhibits/unicode/fullwidth-bypasses-keyword-filter.json +0 -61
- package/src/exhibits/unicode/german-eszett-case-fold.json +0 -72
- package/src/exhibits/unicode/grapheme-vs-codepoint-length.json +0 -57
- package/src/exhibits/unicode/greek-final-sigma-casefold.json +0 -54
- package/src/exhibits/unicode/js-word-regex-ascii-only.json +0 -64
- package/src/exhibits/unicode/nfc-nfd-same-name-two-accounts.json +0 -70
- package/src/exhibits/unicode/nfkc-compatibility-folding.json +0 -63
- package/src/exhibits/unicode/truncation-orphans-combining-mark.json +0 -62
- package/src/exhibits/unicode/truncation-splits-surrogate-pair.json +0 -65
- package/src/exhibits/unicode/turkish-dotless-i-case-mapping.json +0 -72
- package/src/exhibits/unicode/zalgo-combining-mark-stacking.json +0 -57
- package/src/exhibits/unicode/zero-width-space-username.json +0 -62
- package/src/harness.mjs +0 -331
- package/src/index.ts +0 -686
- package/src/server.ts +0 -49
- package/src/verified.json +0 -977
- package/tsconfig.json +0 -18
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "sort-by-surname-particle-convention-mismatch",
|
|
3
|
-
"title": "Sorting people by their stored surname puts \"van der Berg\" after \"de Vries\"; a Dutch phone book would put it before",
|
|
4
|
-
"category": "names-addresses",
|
|
5
|
-
"assumption": "Sorting a list of surnames alphabetically, exactly as the strings are stored, produces the one correct order.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "[\"Bakker\", \"van der Berg\", \"de Vries\"]",
|
|
8
|
-
"description": "A directory, address book, or customer list sorted by a 'last name' field that stores the full surname including any leading particle."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "Sorting the raw strings files \"van der Berg\" under V, after \"de Vries\"; the Dutch convention drops a leading particle (van, der, de) when computing the sort key, filing \"van der Berg\" under B, ahead of \"de Vries\" under V. The two conventions put these two names in a different relative order.",
|
|
11
|
-
"why": "There is no single universal rule for sorting a surname with a leading particle. The Dutch convention, followed by Dutch directories and by library cataloging rules for Dutch names, drops the particle to compute the sort key; Belgian (Flemish) practice and many other systems keep it. A system that just lowercases and sorts the stored string whole keeps the particle in place. The conventions simply disagree, and most software picks the whole-string sort without ever deciding to.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"any 'sort by last name' feature using the raw stored string",
|
|
14
|
-
"library/bibliographic cataloging convention for particles (documented, not universally applied by software)"
|
|
15
|
-
],
|
|
16
|
-
"repros": [
|
|
17
|
-
{
|
|
18
|
-
"language": "python",
|
|
19
|
-
"code": "people = [(\"Pieter\", \"Bakker\"), (\"Joost\", \"van der Berg\"), (\"Anna\", \"de Vries\")]\nnaive_order = sorted(people, key=lambda p: p[1].lower())\nprint([p[1] for p in naive_order])\n\nPARTICLES = {\"van\", \"der\", \"de\", \"von\", \"del\", \"la\"}\ndef catalog_key(surname):\n words = surname.split(\" \")\n while words and words[0].lower() in PARTICLES:\n words.pop(0)\n return \" \".join(words).lower()\n\ncatalog_order = sorted(people, key=lambda p: catalog_key(p[1]))\nprint([p[1] for p in catalog_order])\nprint(naive_order == catalog_order)\n",
|
|
20
|
-
"expected_output": "['Bakker', 'de Vries', 'van der Berg']\n['Bakker', 'van der Berg', 'de Vries']\nFalse",
|
|
21
|
-
"verified_with": "Python 3.14.2"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"language": "javascript",
|
|
25
|
-
"code": "const people = [[\"Pieter\", \"Bakker\"], [\"Joost\", \"van der Berg\"], [\"Anna\", \"de Vries\"]];\nconst naiveOrder = [...people].sort((a, b) => a[1].toLowerCase().localeCompare(b[1].toLowerCase()));\nconsole.log(naiveOrder.map((p) => p[1]));\n\nconst PARTICLES = new Set([\"van\", \"der\", \"de\", \"von\", \"del\", \"la\"]);\nfunction catalogKey(surname) {\n const words = surname.split(\" \");\n while (words.length && PARTICLES.has(words[0].toLowerCase())) words.shift();\n return words.join(\" \").toLowerCase();\n}\nconst catalogOrder = [...people].sort((a, b) => catalogKey(a[1]).localeCompare(catalogKey(b[1])));\nconsole.log(catalogOrder.map((p) => p[1]));\nconsole.log(JSON.stringify(naiveOrder) === JSON.stringify(catalogOrder));\n",
|
|
26
|
-
"expected_output": "[ 'Bakker', 'de Vries', 'van der Berg' ]\n[ 'Bakker', 'van der Berg', 'de Vries' ]\nfalse",
|
|
27
|
-
"verified_with": "Node.js v22.22.2"
|
|
28
|
-
}
|
|
29
|
-
],
|
|
30
|
-
"fix": "Pick a sort-key convention explicitly (whole string vs. particle-dropped) and apply it consistently, rather than sorting by whatever happens to be stored in the surname field, and don't assume there's one universally 'correct' convention; it varies by language and institution.",
|
|
31
|
-
"related": [
|
|
32
|
-
"split-surname-on-last-space-loses-particle"
|
|
33
|
-
],
|
|
34
|
-
"sources": [
|
|
35
|
-
{
|
|
36
|
-
"title": "Patrick McKenzie: Falsehoods Programmers Believe About Names",
|
|
37
|
-
"url": "https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/"
|
|
38
|
-
}
|
|
39
|
-
],
|
|
40
|
-
"tags": [
|
|
41
|
-
"names",
|
|
42
|
-
"sorting",
|
|
43
|
-
"display",
|
|
44
|
-
"i18n",
|
|
45
|
-
"database"
|
|
46
|
-
],
|
|
47
|
-
"test_cases": [
|
|
48
|
-
{
|
|
49
|
-
"inputs": [
|
|
50
|
-
"van der Berg",
|
|
51
|
-
"de Vries"
|
|
52
|
-
],
|
|
53
|
-
"relation": "no_crash",
|
|
54
|
-
"note": "Decide and document which sort-key convention your directory uses for particled surnames; the two common ones disagree."
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "spanish-double-surname-treated-as-single",
|
|
3
|
-
"title": "Taking the last word of \"Lucía Ferrer Montalbán\" as 'the' surname drops a whole surname",
|
|
4
|
-
"category": "names-addresses",
|
|
5
|
-
"assumption": "A person has exactly one surname, which is whichever word comes last in their full name.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "\"Lucía Ferrer Montalbán\" (an invented name)",
|
|
8
|
-
"description": "A full-name field for someone from a Spanish-naming-convention culture, where a person carries both a paternal and a maternal surname, split with a 'last word = surname' heuristic."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "The system records \"Montalbán\" as the surname and silently drops \"Ferrer\", the first (traditionally paternal) surname, which breaks any later attempt to match this person against relatives who share it, or against official documents that list both.",
|
|
11
|
-
"why": "In Spain and most of Spanish-speaking Latin America, a person normally carries TWO surnames, both legally part of the name and neither a middle name; traditionally the first is the father's first surname and the second is the mother's first surname (Spain has let parents choose the order since 1999). A 'last word is the surname' rule, which works for a simple two-part name, discards the first of the two surnames because it has no way to know that this particular name has three meaningful parts, not two.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"any 'last word is the surname' name parser applied to a spanish-convention full name"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "python",
|
|
18
|
-
"code": "full = \"Luc\\u00eda Ferrer Montalb\\u00e1n\"\nparts = full.split(\" \")\ngiven, surname = parts[0], parts[-1]\nprint(\"given:\", given, \"| recorded surname:\", surname, \"| dropped:\", parts[1:-1])\n",
|
|
19
|
-
"expected_output": "given: Lucía | recorded surname: Montalbán | dropped: ['Ferrer']",
|
|
20
|
-
"verified_with": "Python 3.14.2"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"language": "javascript",
|
|
24
|
-
"code": "const full = \"Luc\\u00eda Ferrer Montalb\\u00e1n\";\nconst parts = full.split(\" \");\nconst given = parts[0];\nconst surname = parts[parts.length - 1];\nconsole.log(\"given:\", given, \"| recorded surname:\", surname, \"| dropped:\", parts.slice(1, -1));\n",
|
|
25
|
-
"expected_output": "given: Lucía | recorded surname: Montalbán | dropped: [ 'Ferrer' ]",
|
|
26
|
-
"verified_with": "Node.js v22.22.2"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"fix": "Don't assume a fixed name structure derived from word position; ask for given name(s) and surname(s) as separate, explicitly-labeled fields, and allow more than one surname.",
|
|
30
|
-
"related": [
|
|
31
|
-
"split-surname-on-last-space-loses-particle"
|
|
32
|
-
],
|
|
33
|
-
"sources": [
|
|
34
|
-
{
|
|
35
|
-
"title": "Patrick McKenzie: Falsehoods Programmers Believe About Names",
|
|
36
|
-
"url": "https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/"
|
|
37
|
-
}
|
|
38
|
-
],
|
|
39
|
-
"tags": [
|
|
40
|
-
"names",
|
|
41
|
-
"forms",
|
|
42
|
-
"validation",
|
|
43
|
-
"database",
|
|
44
|
-
"i18n"
|
|
45
|
-
],
|
|
46
|
-
"test_cases": [
|
|
47
|
-
{
|
|
48
|
-
"inputs": [
|
|
49
|
-
"Lucía Ferrer Montalbán"
|
|
50
|
-
],
|
|
51
|
-
"relation": "no_crash",
|
|
52
|
-
"note": "Check whether your surname extraction keeps both 'Ferrer' and 'Montalbán', or silently drops one."
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "split-surname-on-last-space-loses-particle",
|
|
3
|
-
"title": "Splitting a name on the last space doesn't find the surname — it finds the last WORD",
|
|
4
|
-
"category": "names-addresses",
|
|
5
|
-
"assumption": "The text after the last space in a full name is the surname.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "\"Joost van der Berg\" (an invented name)",
|
|
8
|
-
"description": "A 'full name' text field being split into given/surname for a database or a form, using the common 'last word = surname' heuristic."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "The surname field ends up holding just \"Berg\", and \"van der\" — which is part of the actual family name \"van der Berg\" — gets silently folded into the given-name field instead.",
|
|
11
|
-
"why": "Splitting on the last whitespace boundary only knows about whitespace, not about which words belong to the family name. Dutch, German, and other surnames routinely include lowercase particles (van, der, von, de) that are grammatically and legally part of the surname, not separate given names, and a position-based split has no way to know where the surname actually starts.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"any 'split full name on whitespace' form handler",
|
|
14
|
-
"python str.rsplit",
|
|
15
|
-
"javascript lastIndexOf-based split"
|
|
16
|
-
],
|
|
17
|
-
"repros": [
|
|
18
|
-
{
|
|
19
|
-
"language": "python",
|
|
20
|
-
"code": "def naive_split(full_name):\n given, surname = full_name.rsplit(\" \", 1)\n return given, surname\n\nprint(naive_split(\"Joost van der Berg\"))\n",
|
|
21
|
-
"expected_output": "('Joost van der', 'Berg')",
|
|
22
|
-
"verified_with": "Python 3.14.2"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"language": "javascript",
|
|
26
|
-
"code": "function naiveSplit(fullName) {\n const i = fullName.lastIndexOf(\" \");\n return [fullName.slice(0, i), fullName.slice(i + 1)];\n}\nconsole.log(naiveSplit(\"Joost van der Berg\"));\n",
|
|
27
|
-
"expected_output": "[ 'Joost van der', 'Berg' ]",
|
|
28
|
-
"verified_with": "Node.js v22.22.2"
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
"fix": "Collect given-name and family-name as separate fields entered directly by the person instead of inferring the split from word position; if you must infer it, maintain an explicit list of known lowercase surname particles to fold into the family-name side.",
|
|
32
|
-
"related": [
|
|
33
|
-
"mononym-fails-first-last-required",
|
|
34
|
-
"spanish-double-surname-treated-as-single"
|
|
35
|
-
],
|
|
36
|
-
"sources": [
|
|
37
|
-
{
|
|
38
|
-
"title": "Patrick McKenzie: Falsehoods Programmers Believe About Names",
|
|
39
|
-
"url": "https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/"
|
|
40
|
-
}
|
|
41
|
-
],
|
|
42
|
-
"tags": [
|
|
43
|
-
"names",
|
|
44
|
-
"forms",
|
|
45
|
-
"validation",
|
|
46
|
-
"database",
|
|
47
|
-
"i18n",
|
|
48
|
-
"display"
|
|
49
|
-
],
|
|
50
|
-
"test_cases": [
|
|
51
|
-
{
|
|
52
|
-
"inputs": [
|
|
53
|
-
"Joost van der Berg"
|
|
54
|
-
],
|
|
55
|
-
"relation": "no_crash",
|
|
56
|
-
"note": "Check whether your surname field comes out as 'Berg' or 'van der Berg' — most systems get this wrong."
|
|
57
|
-
}
|
|
58
|
-
]
|
|
59
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "title-case-mangles-mixed-case-and-apostrophes",
|
|
3
|
-
"title": "str.title() turns \"mcdonald\" into \"Mcdonald\" and \"they're\" into \"They'Re\"",
|
|
4
|
-
"category": "names-addresses",
|
|
5
|
-
"assumption": "A built-in title-case function always produces the conventionally correct capitalization of a name.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "\"mcdonald\", \"they're\"",
|
|
8
|
-
"description": "A lowercase or all-caps name from a legacy database or a careless import, being 'fixed up' for display with a generic title-casing function."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "\"mcdonald\" displays as \"Mcdonald\" instead of \"McDonald\", and a word containing an apostrophe like \"they're\" gets a capital letter injected right after the apostrophe, becoming \"They'Re\".",
|
|
11
|
-
"why": "Python's str.title() capitalizes the first letter of every maximal run of alphabetic characters and lowercases the rest; it has no dictionary of surname conventions, so it has no way to know the 'D' in McDonald should also be capitalized, and it treats an apostrophe as a word boundary just like a space — which happens to look right for a name like \"o'neil\" but is wrong for a contraction like \"they're\", capitalizing the letter right after the apostrophe either way. The common hand-rolled JavaScript equivalent (capitalize the first character of each whitespace-separated token) fails differently: it also gives \"Mcdonald\", but treats \"o'neil\" as one token and returns \"O'neil\", missing the capital that name needs.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"python str.title()"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "python",
|
|
18
|
-
"code": "print(\"mcdonald\".title())\nprint(\"o'neil\".title())\nprint(\"they're\".title())\n",
|
|
19
|
-
"expected_output": "Mcdonald\nO'Neil\nThey'Re",
|
|
20
|
-
"verified_with": "Python 3.14.2"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"language": "javascript",
|
|
24
|
-
"code": "function titleCase(s) {\n return s.replace(/\\w\\S*/g, (t) => t[0].toUpperCase() + t.slice(1).toLowerCase());\n}\nconsole.log(titleCase(\"mcdonald\"));\nconsole.log(titleCase(\"o'neil\"));\n",
|
|
25
|
-
"expected_output": "Mcdonald\nO'neil",
|
|
26
|
-
"verified_with": "Node.js v22.22.2"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"fix": "Don't auto-capitalize a name with a generic title-casing function; either store and display the name exactly as the person entered it, or maintain an explicit exception list for known patterns (Mc/Mac, O', particles) and accept that no automatic rule covers every name.",
|
|
30
|
-
"related": [
|
|
31
|
-
"ascii-regex-rejects-accented-and-hyphenated-names"
|
|
32
|
-
],
|
|
33
|
-
"sources": [
|
|
34
|
-
{
|
|
35
|
-
"title": "Python docs: str.title()",
|
|
36
|
-
"url": "https://docs.python.org/3/library/stdtypes.html#str.title"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"title": "Patrick McKenzie: Falsehoods Programmers Believe About Names",
|
|
40
|
-
"url": "https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/"
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
"tags": [
|
|
44
|
-
"names",
|
|
45
|
-
"display",
|
|
46
|
-
"validation",
|
|
47
|
-
"forms",
|
|
48
|
-
"i18n"
|
|
49
|
-
],
|
|
50
|
-
"test_cases": [
|
|
51
|
-
{
|
|
52
|
-
"inputs": [
|
|
53
|
-
"mcdonald"
|
|
54
|
-
],
|
|
55
|
-
"relation": "returns",
|
|
56
|
-
"expected": "McDonald",
|
|
57
|
-
"note": "A generic title-case function will almost certainly fail this — decide whether you want to auto-capitalize names at all before relying on one."
|
|
58
|
-
}
|
|
59
|
-
]
|
|
60
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "uk-canada-postal-code-assumed-numeric",
|
|
3
|
-
"title": "A 'postal code must be 5 digits' check rejects every UK and Canadian address",
|
|
4
|
-
"category": "names-addresses",
|
|
5
|
-
"assumption": "A postal/zip code is always a short string of digits.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "\"SW1A 1AA\" (UK), \"K1A 0B1\" (Canada)",
|
|
8
|
-
"description": "A postal code field validated against a fixed numeric pattern, written with only US ZIP codes in mind."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "Perfectly valid UK and Canadian postal codes are rejected outright because they mix letters and digits and don't match a 5-digit pattern at all.",
|
|
11
|
-
"why": "US ZIP codes happen to be exactly 5 digits, but that's a US-specific format, not a property of 'postal codes' in general. UK postcodes (like SW1A 1AA) and Canadian postal codes (like K1A 0B1) are alphanumeric by design, with letter-digit structures that a 5-digit-only regex has no way to match.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"any postal-code validator hard-coded to \\d{5}"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "python",
|
|
18
|
-
"code": "import re\npattern = re.compile(r\"^\\d{5}$\")\nfor code in [\"02134\", \"SW1A 1AA\", \"K1A 0B1\"]:\n print(code, bool(pattern.match(code)))\n",
|
|
19
|
-
"expected_output": "02134 True\nSW1A 1AA False\nK1A 0B1 False",
|
|
20
|
-
"verified_with": "Python 3.14.2"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"language": "javascript",
|
|
24
|
-
"code": "const pattern = /^\\d{5}$/;\nfor (const code of [\"02134\", \"SW1A 1AA\", \"K1A 0B1\"]) {\n console.log(code, pattern.test(code));\n}\n",
|
|
25
|
-
"expected_output": "02134 true\nSW1A 1AA false\nK1A 0B1 false",
|
|
26
|
-
"verified_with": "Node.js v22.22.2"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"fix": "Validate postal codes per-country using that country's actual format (or accept any reasonably short alphanumeric string if the country isn't known), never a single 5-digit pattern applied worldwide.",
|
|
30
|
-
"related": [
|
|
31
|
-
"us-zip-code-leading-zero-lost-as-integer"
|
|
32
|
-
],
|
|
33
|
-
"sources": [
|
|
34
|
-
{
|
|
35
|
-
"title": "Michael Tandy: Falsehoods Programmers Believe About Addresses",
|
|
36
|
-
"url": "https://www.mjt.me.uk/posts/falsehoods-programmers-believe-about-addresses/"
|
|
37
|
-
}
|
|
38
|
-
],
|
|
39
|
-
"tags": [
|
|
40
|
-
"addresses",
|
|
41
|
-
"validation",
|
|
42
|
-
"forms",
|
|
43
|
-
"i18n",
|
|
44
|
-
"database"
|
|
45
|
-
],
|
|
46
|
-
"test_cases": [
|
|
47
|
-
{
|
|
48
|
-
"inputs": [
|
|
49
|
-
"SW1A 1AA"
|
|
50
|
-
],
|
|
51
|
-
"relation": "no_crash",
|
|
52
|
-
"note": "A valid UK postcode should be accepted by your postal-code field."
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "us-zip-code-leading-zero-lost-as-integer",
|
|
3
|
-
"title": "A Boston ZIP code, \"02134\", becomes 2134 the moment anything parses it as a number",
|
|
4
|
-
"category": "names-addresses",
|
|
5
|
-
"assumption": "A ZIP code can be safely stored or parsed as an integer; it's 'just a number' after all.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "\"02134\" parsed with int() / parseInt()",
|
|
8
|
-
"description": "A US ZIP code stored in a numeric database column, or passed through a spreadsheet import / schema that infers 'number' for a 5-digit-looking string."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "The leading zero silently disappears, turning a real, valid ZIP code into a 4-digit number that fails every later format check and no longer matches the original address.",
|
|
11
|
-
"why": "A ZIP code is an identifier made of digit CHARACTERS, not a numeric quantity; there's no meaningful arithmetic anyone would do with it. int()/parseInt()/Number() all discard leading zeros because they parse decimal numeric VALUE, and the value of 02134 is mathematically identical to 2134 — the leading zero carries no numeric information, but it is semantically part of the identifier.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"python int()",
|
|
14
|
-
"javascript parseInt()/Number()",
|
|
15
|
-
"any database column typed as an integer for a zip/postal code"
|
|
16
|
-
],
|
|
17
|
-
"repros": [
|
|
18
|
-
{
|
|
19
|
-
"language": "python",
|
|
20
|
-
"code": "zip_code = \"02134\"\nas_int = int(zip_code)\nprint(as_int, str(as_int))\nprint(str(as_int) == zip_code)\n",
|
|
21
|
-
"expected_output": "2134 2134\nFalse",
|
|
22
|
-
"verified_with": "Python 3.14.2"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"language": "javascript",
|
|
26
|
-
"code": "const zipCode = \"02134\";\nconst asInt = parseInt(zipCode, 10);\nconsole.log(asInt, String(asInt));\nconsole.log(String(asInt) === zipCode);\n",
|
|
27
|
-
"expected_output": "2134 2134\nfalse",
|
|
28
|
-
"verified_with": "Node.js v22.22.2"
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
"fix": "Store postal codes as strings, never as a numeric type, and never round-trip them through int()/parseInt()/Number() — there is nothing numeric about an identifier that happens to look like digits.",
|
|
32
|
-
"related": [
|
|
33
|
-
"uk-canada-postal-code-assumed-numeric"
|
|
34
|
-
],
|
|
35
|
-
"sources": [
|
|
36
|
-
{
|
|
37
|
-
"title": "Michael Tandy: Falsehoods Programmers Believe About Addresses",
|
|
38
|
-
"url": "https://www.mjt.me.uk/posts/falsehoods-programmers-believe-about-addresses/"
|
|
39
|
-
}
|
|
40
|
-
],
|
|
41
|
-
"tags": [
|
|
42
|
-
"addresses",
|
|
43
|
-
"parsing",
|
|
44
|
-
"database",
|
|
45
|
-
"validation",
|
|
46
|
-
"forms"
|
|
47
|
-
],
|
|
48
|
-
"test_cases": [
|
|
49
|
-
{
|
|
50
|
-
"inputs": [
|
|
51
|
-
"02134"
|
|
52
|
-
],
|
|
53
|
-
"relation": "returns",
|
|
54
|
-
"expected": "02134",
|
|
55
|
-
"note": "Round-tripping a ZIP code through your storage layer should preserve the leading zero."
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "varchar-truncation-splits-name-mid-grapheme",
|
|
3
|
-
"title": "Truncating a name to a fixed length can cut a letter in half, dropping its accent silently",
|
|
4
|
-
"category": "names-addresses",
|
|
5
|
-
"assumption": "Truncating a string to N characters always leaves N valid, complete characters.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "\"Nguy\\u1ec5n V\\u0103n H\\u01b0ng\" (an invented, common-pattern Vietnamese name) normalized to NFD, truncated to 11 code points",
|
|
8
|
-
"description": "A name stored in decomposed Unicode form (common after certain input methods or normalization steps) being cut down to fit a fixed-width column or display field."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "The cut lands between a base letter and its combining accent mark, silently dropping the accent and turning \"Văn\" into \"Va\", with no error, warning, or visible corruption in the truncation step itself.",
|
|
11
|
-
"why": "In NFD (decomposed) form, an accented letter like ă is represented as TWO code points: the base letter 'a' followed by a separate combining breve mark. A length-based slice has no concept of 'this code point belongs with the previous one'; it just counts positions, so a cut that lands exactly between the base letter and its combining mark keeps the bare base letter and discards the accent, producing a different, valid-looking string that is quietly wrong rather than visibly broken.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"any code-point-count truncation (string slicing, SQL LEFT()/SUBSTRING(), an explicit cast to varchar(n)) applied to NFD text",
|
|
14
|
-
"python string slicing",
|
|
15
|
-
"javascript string slicing"
|
|
16
|
-
],
|
|
17
|
-
"repros": [
|
|
18
|
-
{
|
|
19
|
-
"language": "python",
|
|
20
|
-
"code": "import unicodedata\nname = unicodedata.normalize(\"NFD\", \"Nguy\\u1ec5n V\\u0103n H\\u01b0ng\")\nprint(\"full (NFC):\", unicodedata.normalize(\"NFC\", name))\ntruncated = name[:11]\nprint(\"truncated (NFC):\", unicodedata.normalize(\"NFC\", truncated))\n",
|
|
21
|
-
"expected_output": "full (NFC): Nguyễn Văn Hưng\ntruncated (NFC): Nguyễn Va",
|
|
22
|
-
"verified_with": "Python 3.14.2"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"language": "javascript",
|
|
26
|
-
"code": "const name = \"Nguy\\u1ec5n V\\u0103n H\\u01b0ng\".normalize(\"NFD\");\nconsole.log(\"full (NFC):\", name.normalize(\"NFC\"));\nconst truncated = name.slice(0, 11);\nconsole.log(\"truncated (NFC):\", truncated.normalize(\"NFC\"));\n",
|
|
27
|
-
"expected_output": "full (NFC): Nguyễn Văn Hưng\ntruncated (NFC): Nguyễn Va",
|
|
28
|
-
"verified_with": "Node.js v22.22.2"
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
"fix": "Normalize to NFC before storing or measuring length (precomposed accented letters are single code points, far less likely to be split), and when you must truncate, cut at a grapheme-cluster boundary, not a raw code-point or UTF-16-unit count.",
|
|
32
|
-
"related": [
|
|
33
|
-
"nfc-nfd-same-name-two-accounts",
|
|
34
|
-
"nfc-nfd-filename-lookup-mismatch"
|
|
35
|
-
],
|
|
36
|
-
"sources": [
|
|
37
|
-
{
|
|
38
|
-
"title": "Unicode Standard Annex #29: Unicode Text Segmentation (grapheme clusters)",
|
|
39
|
-
"url": "https://www.unicode.org/reports/tr29/"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"title": "Unicode Standard Annex #15: Unicode Normalization Forms",
|
|
43
|
-
"url": "https://www.unicode.org/reports/tr15/"
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
"tags": [
|
|
47
|
-
"names",
|
|
48
|
-
"truncation",
|
|
49
|
-
"length validation",
|
|
50
|
-
"database",
|
|
51
|
-
"i18n",
|
|
52
|
-
"unicode"
|
|
53
|
-
],
|
|
54
|
-
"test_cases": [
|
|
55
|
-
{
|
|
56
|
-
"inputs": [
|
|
57
|
-
"Nguyễn Văn Hưng"
|
|
58
|
-
],
|
|
59
|
-
"relation": "no_crash",
|
|
60
|
-
"note": "Feed your truncation helper this name in NFD form with a limit of 11: a grapheme-aware truncation keeps \"Nguyễn V\" or \"Nguyễn Vă\", never a bare \"a\" stripped of its breve."
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "csv-crlf-trailing-blank-line",
|
|
3
|
-
"title": "A trailing blank line in a CSV file parses as a real (empty) row, and indexing into it throws",
|
|
4
|
-
"category": "parsing",
|
|
5
|
-
"assumption": "A CSV file's trailing newline (or a blank line left at the end by whatever produced it) just gets dropped, so the number of parsed rows always equals the number of visible data lines.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "a,b\\n1,2\\n\\n (note the extra blank line before end of file)",
|
|
8
|
-
"description": "A CSV generated by exporting from a spreadsheet or database tool that leaves one truly empty line at the end of the file, as opposed to a single ordinary trailing newline after the last data row (which is harmless)."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "A single trailing line ending after the last row (`a,b\\r\\n1,2\\r\\n3,4\\r\\n`) parses cleanly as three rows - but an extra blank line (`a,b\\n1,2\\n\\n`) parses as three rows in Python where the last one is a genuinely empty list `[]`, so `rows[-1][0]` raises `IndexError: list index out of range`. The JS naive `split(\"\\n\")` equivalent doesn't crash on this particular shape, but produces two phantom `[\"\"]` rows.",
|
|
11
|
-
"why": "A CSV reader has to distinguish \"the file ends after a newline\" (no extra row) from \"there is a genuinely empty line in the data\" (a zero-field row). Python's `csv` module does the former correctly and represents the latter faithfully as `[]`, which is correct behavior but a shape callers rarely expect to handle, so indexing into the last row without checking its length crashes. Plain `text.split(\"\\n\")` in JavaScript produces a trailing empty string element whenever the text ends in a newline at all, so naive code gets an extra `[\"\"]`-shaped phantom row even for the harmless single-trailing-newline case that Python's real parser handles cleanly.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"python",
|
|
14
|
-
"javascript"
|
|
15
|
-
],
|
|
16
|
-
"repros": [
|
|
17
|
-
{
|
|
18
|
-
"language": "python",
|
|
19
|
-
"code": "import csv, io\nclean = 'a,b\\r\\n1,2\\r\\n3,4\\r\\n'\nprint(list(csv.reader(io.StringIO(clean))))\nwith_blank = 'a,b\\n1,2\\n\\n'\nrows = list(csv.reader(io.StringIO(with_blank)))\nprint(rows)\ntry:\n print(rows[-1][0])\nexcept IndexError as e:\n print(\"IndexError:\", e)\n",
|
|
20
|
-
"expected_output": "[['a', 'b'], ['1', '2'], ['3', '4']]\n[['a', 'b'], ['1', '2'], []]\nIndexError: list index out of range",
|
|
21
|
-
"verified_with": "Python 3.14.2"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"language": "javascript",
|
|
25
|
-
"code": "const withBlank = \"a,b\\n1,2\\n\\n\";\nconst naiveRows = withBlank.split(\"\\n\").map(line => line.split(\",\"));\nconsole.log(JSON.stringify(naiveRows));\nconsole.log(naiveRows[naiveRows.length - 1][0]);\n",
|
|
26
|
-
"expected_output": "[[\"a\",\"b\"],[\"1\",\"2\"],[\"\"],[\"\"]]",
|
|
27
|
-
"verified_with": "Node.js v22.22.2"
|
|
28
|
-
}
|
|
29
|
-
],
|
|
30
|
-
"fix": "Filter out zero-length rows (Python: `if row` after reading) or trailing empty lines before splitting (JS), and never index into the last parsed row without checking it has the expected number of fields first.",
|
|
31
|
-
"related": [
|
|
32
|
-
"csv-utf8-bom-first-header"
|
|
33
|
-
],
|
|
34
|
-
"sources": [
|
|
35
|
-
{
|
|
36
|
-
"title": "Python docs: csv - CSV File Reading and Writing",
|
|
37
|
-
"url": "https://docs.python.org/3/library/csv.html"
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"title": "RFC 4180: Common Format and MIME Type for Comma-Separated Values (CSV) Files",
|
|
41
|
-
"url": "https://www.rfc-editor.org/rfc/rfc4180"
|
|
42
|
-
}
|
|
43
|
-
],
|
|
44
|
-
"tags": [
|
|
45
|
-
"csv",
|
|
46
|
-
"upload",
|
|
47
|
-
"parsing",
|
|
48
|
-
"validation"
|
|
49
|
-
],
|
|
50
|
-
"test_cases": [
|
|
51
|
-
{
|
|
52
|
-
"inputs": [
|
|
53
|
-
"a,b\n1,2\n\n"
|
|
54
|
-
],
|
|
55
|
-
"relation": "returns",
|
|
56
|
-
"expected": [
|
|
57
|
-
[
|
|
58
|
-
"a",
|
|
59
|
-
"b"
|
|
60
|
-
],
|
|
61
|
-
[
|
|
62
|
-
"1",
|
|
63
|
-
"2"
|
|
64
|
-
],
|
|
65
|
-
[]
|
|
66
|
-
],
|
|
67
|
-
"note": "A CSV-parsing function given a file with a genuinely blank trailing line should represent it as an empty row (as Python's csv.reader does) rather than crashing - callers must then check row length before indexing."
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
"inputs": [
|
|
71
|
-
"a,b\r\n1,2\r\n"
|
|
72
|
-
],
|
|
73
|
-
"relation": "returns",
|
|
74
|
-
"expected": [
|
|
75
|
-
[
|
|
76
|
-
"a",
|
|
77
|
-
"b"
|
|
78
|
-
],
|
|
79
|
-
[
|
|
80
|
-
"1",
|
|
81
|
-
"2"
|
|
82
|
-
]
|
|
83
|
-
],
|
|
84
|
-
"note": "A single CRLF after the last row is just the line terminator: exactly two rows, no phantom empty row and no stray \\r in the last field."
|
|
85
|
-
}
|
|
86
|
-
]
|
|
87
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "csv-doubled-quote-escaping",
|
|
3
|
-
"title": "A literal quote mark inside a CSV field is written as two quote marks, not a backslash-escaped one",
|
|
4
|
-
"category": "parsing",
|
|
5
|
-
"assumption": "A quote character inside a CSV field is escaped the way it is in most languages, with a backslash.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "name,quote\\n\"Say \"\"hello\"\"\",5\\n",
|
|
8
|
-
"description": "A CSV export of a field whose text contains a literal double-quote character, e.g. a product name like `Say \"hello\"` or a quoted phrase inside a review."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "A field containing a literal `\"` is represented in CSV as `\"\"` (two quote characters) inside the surrounding quoted field, not `\\\"` - code that naively strips a single leading/trailing quote and un-escapes backslash-quote sequences (a common pattern borrowed from JSON/string literals) will mangle the field instead of recovering `Say \"hello\"`.",
|
|
11
|
-
"why": "RFC 4180's escape mechanism for a literal double-quote inside a quoted field is to double it - `\"\"` means one literal `\"` - with no backslash involved anywhere in the format. A real CSV parser (Python's `csv` module) recognizes a `\"\"` sequence while inside a quoted field as an escaped quote and collapses it to one `\"` in the field's value; it is a dialect-specific rule that has nothing to do with how any programming language escapes string literals.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"javascript",
|
|
14
|
-
"any backslash-escape-based CSV handling"
|
|
15
|
-
],
|
|
16
|
-
"repros": [
|
|
17
|
-
{
|
|
18
|
-
"language": "python",
|
|
19
|
-
"code": "import csv, io\ntext = 'name,quote\\n\"Say \"\"hello\"\"\",5\\n'\nprint(list(csv.reader(io.StringIO(text))))\n",
|
|
20
|
-
"expected_output": "[['name', 'quote'], ['Say \"hello\"', '5']]",
|
|
21
|
-
"verified_with": "Python 3.14.2"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"language": "javascript",
|
|
25
|
-
"code": "const text = 'name,quote\\n\"Say \"\"hello\"\"\",5\\n';\n// naive: strip outer quotes, un-escape backslash-quote (wrong model for CSV)\nconst line = text.split(\"\\n\")[1];\nconst naiveFields = line.split(\",\");\nconsole.log(JSON.stringify(naiveFields));\n",
|
|
26
|
-
"expected_output": "[\"\\\"Say \\\"\\\"hello\\\"\\\"\\\"\",\"5\"]",
|
|
27
|
-
"verified_with": "Node.js v22.22.2"
|
|
28
|
-
}
|
|
29
|
-
],
|
|
30
|
-
"fix": "Use a real CSV parser that implements RFC 4180's doubled-quote escaping, rather than any backslash-based unescaping borrowed from another format. If writing CSV by hand, double every `\"` inside a field and wrap the whole field in quotes.",
|
|
31
|
-
"related": [
|
|
32
|
-
"csv-quoted-field-with-comma",
|
|
33
|
-
"csv-quoted-field-with-newline"
|
|
34
|
-
],
|
|
35
|
-
"sources": [
|
|
36
|
-
{
|
|
37
|
-
"title": "RFC 4180: Common Format and MIME Type for Comma-Separated Values (CSV) Files",
|
|
38
|
-
"url": "https://www.rfc-editor.org/rfc/rfc4180"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"title": "Python docs: csv - CSV File Reading and Writing",
|
|
42
|
-
"url": "https://docs.python.org/3/library/csv.html"
|
|
43
|
-
}
|
|
44
|
-
],
|
|
45
|
-
"tags": [
|
|
46
|
-
"csv",
|
|
47
|
-
"upload",
|
|
48
|
-
"parsing",
|
|
49
|
-
"validation"
|
|
50
|
-
],
|
|
51
|
-
"test_cases": [
|
|
52
|
-
{
|
|
53
|
-
"inputs": [
|
|
54
|
-
"name,quote\n\"Say \"\"hello\"\"\",5\n"
|
|
55
|
-
],
|
|
56
|
-
"relation": "returns",
|
|
57
|
-
"expected": [
|
|
58
|
-
[
|
|
59
|
-
"name",
|
|
60
|
-
"quote"
|
|
61
|
-
],
|
|
62
|
-
[
|
|
63
|
-
"Say \"hello\"",
|
|
64
|
-
"5"
|
|
65
|
-
]
|
|
66
|
-
],
|
|
67
|
-
"note": "A CSV-parsing function given this text must collapse the doubled quotes into one literal quote character inside the field value."
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
}
|