Bitten by Unicode
pyatl.dev
pyatl.dev
Here's some more,
2010 ; 002D ; MA #* ( ‐ → - ) HYPHEN → HYPHEN-MINUS #
2011 ; 002D ; MA #* ( ‑ → - ) NON-BREAKING HYPHEN → HYPHEN-MINUS #
2012 ; 002D ; MA #* ( ‒ → - ) FIGURE DASH → HYPHEN-MINUS #
2013 ; 002D ; MA #* ( – → - ) EN DASH → HYPHEN-MINUS #
FE58 ; 002D ; MA #* ( ﹘ → - ) SMALL EM DASH → HYPHEN-MINUS #
06D4 ; 002D ; MA #* ( ۔ → - ) ARABIC FULL STOP → HYPHEN-MINUS # →‐→
2043 ; 002D ; MA #* ( ⁃ → - ) HYPHEN BULLET → HYPHEN-MINUS # →‐→
02D7 ; 002D ; MA #* ( ˗ → - ) MODIFIER LETTER MINUS SIGN → HYPHEN-MINUS #
2212 ; 002D ; MA #* ( − → - ) MINUS SIGN → HYPHEN-MINUS #
2796 ; 002D ; MA #* ( → - ) HEAVY MINUS SIGN → HYPHEN-MINUS # →−→
2CBA ; 002D ; MA # ( Ⲻ → - ) COPTIC CAPITAL LETTER DIALECT-P NI → HYPHEN-MINUS # →‒→
copied from https://www.unicode.org/Public/security/8.0.0/confusables.tx...Maybe highlighting isn't such bad idea :)
But the most shocking thing are printed learning resources on things like Haskell where the code examples on purpose are some kind of typographic printout rather than just the symbols themselves!
When asked to render an Unicode character that isn’t present in the font modern OSes will automatically pick a font that has it.
https://en.wikipedia.org/wiki/Fallback_font: “A fallback font is a reserve typeface containing symbols for as many Unicode characters as possible. When a display system encounters a character that is not part of the repertoire of any of the other available fonts, a symbol from a fallback font is used instead. Typically, a fallback font will contain symbols representative of the various types of Unicode characters.”
That can be avoided, for example by storing text as “one character per byte”, but I don’t think many editors do that nowadays.
(Granted this is heavily biased since so much source code is ASCII-only so you don’t get many Unicode problems in the first place...)
On a project targeted at a non English speaking devs with a strong domain knowledge requirement, writing the test patterns (endless arrays of input -> expected output sequences, interspersed with adjustment code) in the native language saves an incredible amount of time and effort, in particular as we don't need to translate obscure notions into even more obscure English.
And that had very little downsides as it's not production running code, lining will still raise anything problematic, and and the whole thing is easier to get reviewed by non domain experts.
We could have made a translation layer to have the content in a spreadsheet and convert it to test code, but that's not any more stable than having unicode names straight into the code.
I agree, and think a decent linter can deal with these issues, and syntax highlighting as well.
In particular these kind of rules tend to get complicated with many exceptions (down to specific folders needing dedicated rules), so doing it as lint and not at the language level gives a lot of freedom on where and how to apply the rules and raise warnings.
x²
as well as italic and bold characters (would have demoed but HN filters out Unicode bold & italics) and I can write a test named processes中文Characters()
and also write Java that looks like APL, add sigil characters in code generated stubs that will never conflict with other people's code because they're too afraid to use these characters, etc.https://github.com/paulhoule/ferocity/blob/main/ferocity-std...
People will ask "how do you enter those characters?" and I say "I don't know but I can cut and paste them, they get offered by the autocomplete, etc."
How to enter them? Well, the keys are on the keyboard.
I have a lot of personal interest in Chinese language content these days, I have no idea how to set up and use an "input method" but I either see the text I want in front of me or ask an LLM "How do I write X in Chinese?" and either way cut and paste.
https://github.com/qmk/qmk_firmware/compare/master...perkee:...
I still use tons of box drawing characters in comments. I'm actually writing a little doodad to let me edit them fluidly then copy them into my block comments, because a truth table is easy to read!
Your comment also reminds me of the introduction of type parameters/generics in Go via the Canadian Aboriginal syllabary for "po" and "pa" ("ᐸ" and "ᐳ") https://github.com/vasilevp/aboriginal
There are honestly so many data-cleaning steps a pipeline could need / have to produce programatically well-formatted data.
2212 MINUS SIGN
2796 HEAVY MINUS SIGN
02D7 MODIFIER LETTER MINUS SIGN
Seven Dashes for the Dash-lords in their quotes as shown, 2012 FIGURE DASH
2013 EN DASH
2014 EM DASH
2015 QUOTATION DASH
2E3A TWO-EM DASH
2E3B THREE-EM DASH
FE58 SMALL EM DASH
Nine Hyphens for Word Breakers, one of them ­, 00AD SOFT HYPHEN
058A ARMENIAN HYPHEN
1400 CANADIAN SYLLABICS HYPHEN
1806 MONGOLIAN TODO SOFT HYPHEN
2010 HYPHEN
2011 NON-BREAKING HYPHEN
2E17 DOUBLE OBLIQUE HYPHEN
2E40 DOUBLE HYPHEN
30A0 KATAKANA-HIRAGANA DOUBLE HYPHEN
One for the Dark Word in the QWERTY zoneIn the land of ASCII where Basic Latin lie.
One String to rule them all, One String to find them,
One String to bring them all and in the plain-text, bind them
In the land of ASCII where Basic Latin lie.
002D HYPHEN-MINUS
- @FakeUnicode on Twitter, with apologies to J. R. R. TolkienFor what it's worth TFA could still use a regexp, it would just be slightly more complex. But the conditional statement may or may not be faster or easier to reason with.
You used to get people writing web pages in Microsoft Word, a tool designed for human prose, and so has "smart quotes" on by default, hence they write:
<div class=“a b c d”>
which is parsed as: <div class="“a" b="" c="" d”="">
because smart quotes aren't quotes. The author used the wrong tool for composing text. They should have used a text editor.I also find that even people in text editors sometimes accidentally type some combination that is invisibly wrong, for example Option+Space on macOS is a non-breaking space (U+00A0) rather than regular space (U+0020) and that's quite easy to type accidentally, especially If You're Adding Capitals because shift and option are pretty near each other.
Sometimes people also manage to insert carriage returns and/or linefeeds in what's supposed to be a single-line input value, so regular expressions using "." to match anything don't go beyond the first newline unless you turn on the "multiline" flag.
None of this is unicode specifically, it's just the age old problem of human ingenuity in providing nonstandard data, and whether _you_ do workarounds to fix it, or you make the supplier fix it.
Then you have the opposite problem: most text editors make it non-trivial to work with unicode. I mean, I've taken the time to learn how to type curly quotation marks vs. straight ones, but not everyone has and keyboards don't make it easy.
- Curly quotes are a typographic sugar that's easier on the human eye when reading normal, human-language text. It's reasonable for them to be automatically inserted into your typing in something like a word processor, and depending on which language you're writing in, there may be strong orthographic rules about the use of curly braces (or their cognates, like « guillemets », etc).
- Straight quotes belong in code by a combination of convention and practicality; unicode characters should be escaped wherever it's practical to do so (for example, if you must use "→" in your code, prefer to do e.g. "\u2192" instead -- it's clearer for future users which exact unicode arrow you were using there).
also i sometimes accidentally replace a " with “ or ”, or a ' with a ‘ or ’, inside of `` or an indented code block
For here, assuming you already have only have the numeric value as a token, the regex should look like
/ ^ -? [0-9]+ ( \. [0-9]+ )? $ /x
or something similar. Match the beginning and end of the string and everything inbetween: an optional hyphen, any number of digits, and an optional decimal conponent. Feel free to adjust the details to match your spec, but any unexpected character will fail to parse.For instance, you likely want to accept locale-specific numerals, and any of 77 ٧ ৭ 七 match the \d character class and mean "seven", but you likely don't want to accept a string as a valid number if different types of digits are mixed together.
Also, 1,23,456.78 is fine in an Indian locale, but likely is a typo in the en_US or en_UK locales.
That depends on locale. Is "1,222" 1222 or 1.222?
https://www.unicode.org/reports/tr39/
Here's the relevant bit from the doc:
> For an input string X, define skeleton(X) to be the following transformation on the string:
Convert X to NFD format, as described in [UAX15].
Remove any characters in X that have the property Default_Ignorable_Code_Point.
Concatenate the prototypes for each character in X according to the specified data, producing a string of exemplar characters.
Reapply NFD.
The strings X and Y are defined to be confusable if and only if skeleton(X) = skeleton(Y). This is abbreviated as X ≅ Y.This is obviously talking about comparing two string to see if they are "confusable" but if you just run the skeleton function on a string, you get a "normalize" version of it.
But in fact, none of the unicode normalization forms seem to convert a `HYPHEN` to a `HYPHEN-MINUS`. Try it, you'll see!
Unicode considers them semantically different characters, and not normalized.
The default normalization forms NFC and NFD that are probably defaults for a "unicode normalize" function will should always result in exactly equivalent glyphs (displayed the same by a given font modulo bugs), just expressed differently in unicode. Like single code point "Latin Small Letter E with Acute" (composed, NFC form); vs two code points "latin small letter e" plus "combining acute accent" (decomposed, NFD form). I would not expect them to change the hyphen characters here -- and they do not.
The "compatibility" normalizations, abbreviated by "K" since "C" was already taken for "composed", WILL change glyphs. For instance, they will normalize a "Superscript One" `¹` or a "Circled Digit 1" `①` to an ordinary "Digit 1" (ascii 49). (which could also be relevant to this problem, and it's important all platforms expose compatibility normalization too!) NFKC for compatibility plus composed, or NFKD for compatibility plus decomposed. I expected/hoped they would change the unicode `HYPHEN` to the ascii `HYPHEN-MINUS` here.
But they don't seem to, the unicode directory decided these were not semantically equivalent even at "compatibility" level.
Unfortunately! I was hoping compatibility normalization would solve it too! The standard unicode normalization forms will not resolve this problem though.
(I forget if there are some locale-specific compatibility normalizations? And if so, maybe they would normalize this? I think of compat normalization as usually being like "for search results should it match" (sure you want `1` to match `①`), which can definitely be locale specific)
[1] https://www.unicode.org/Public/security/8.0.0/confusables.tx...
So, yeah, that technical report is about security, typically the potential problems of making a username or domain name or other identifier look like another.
While OP wasn't about security, it does sound like the mapping potentially has non-security uses too as in OP.
(The term "normalization" with regard to unicode usually means something else, specifically NFC, NFD, NFKC, or NFKD normalization from UAX#15, making this hard to talk about clearly, not sure what word to use for this "confusables" mapping).
I haven't actually seen this particular algorithm/mapping discussed before. I'm not sure if routines to perform the mapping are available on common languages/platforms (ruby, python, node, java) -- if someone knows how to do it with, say, Java ICU4J library, it would be useful to see an example.
The confusables.txt file provided does look like it would make it easy to implement the mapping algorithm. I might give it a stab in ruby.
It's a bit confusing to think about what non-security contexts it's applicable without removing semantics you'd want.
In, fact, TR39 says "The strings skeleton(X) and skeleton(Y) are not intended for display, storage or transmission," it's not totally clear if they'd think it was a good idea to use it in OP use case?
If anyone has seen any writing on, or has any thoughts on, how to approach thinking about what non-security use cases and contexts doing this international "confusables" mapping is appropriate vs loss of semantics, I'd love to see it! Like I'm trying to think of whether you might want to map down these "confusables" for search indexing; it also seems like in some cases, especially without locale-specific data, you might be losing semantics you want to keep by doing this.
"What would I get for this share?" can be answered with a float.
"What did I get for selling this share?" should probably be a fixed point value.
I typically use the smaller unit of a currency to store transaction amounts. E.g., for a US transaction of $10, I would store the integer 1000 because that is 1000 cents.
Having done some work on pensions and insurance point-of-sale illustrations & related in the 00s and very early 10s, I'd say that is not correct, at least not here in the UK. Almost everything was specified as intermediate values to 4dp (so hundredths of a penny) and rounded to 2dp for final results.
Though it wasn't consistent: one of the problems we experienced was that while actuarial departments were following this rule others modelling for themselves in Excel or using online calculators (written in JS) were not (as those are all based on IEEE double-precision floats by default, you have to scale and manage scaled ints yourself to get accurate 4dp decimal) so we'd get reports that our calcs were off compared to what the planner's workbook gave (over the length of a pension the rounding errors can compound to quite a noticeable difference).
I've not worked in that are for well over a decade now so maybe things have changed towards floats being the default, but that seems odd to my as it isn't an industry that tends to be happy with reducing precision.
At any rate, I find the conclusion a bit hilarious: "Ah, the input text uses that symbol that very explicitly DOES NOT MEAN 'minus', it _ONLY_ means hyphen, and would be _the_ unicode solution if for whatever reason you want to render the notion: Hyphen followed by a _positive_ cash amount".. and.. I will completely mess up the whole point and just treat it as a minus sign after all.
What, pray tell, is the point of having all those semantically different but visually identical things in unicode when folks don't even acknowledge that what they are doing is fundamentally at odds with the very text they are reading?
As ks2048 points out, the only thing for it is to collect 'em all.
Which is why (shameless plug) I wrote this: https://github.com/mnemnion/runeset
It documented it, at most.
>>> unicodedata.category('\N{MINUS SIGN}')
'Sm'
There you go. No need to thank me for breaking your code.Also, nobody has yet commented on the fact that the author is also doing PDF text extraction. That's yet another area where a lot of fuzziness needs to be applied. My confidence in the author's product greatly decreased after reading this post.
Negative numbers are sometimes represented with parentheses: (234.58)
Tables sometimes tell you in the description that all numbers in are in 1000's or millions.
The dollar sign is used by many currencies, including in Australia and Canada.
I'd probably look around for some other gotchas. Here's one page on prices in general: https://gist.github.com/rgs/6509585 but interestingly doesn't quite cover the OP's problem or the ones I brought up, though the use cases are slightly different.
Edit: pasta above was actually meant to be paste, but gesture input is fun. Ironically it's better this way.
if is_hyphen(value[0]) and value[1] == "$":
converted_value = float(re.sub(r"[^.0-9]", "", value)) \* -1
If the strategy is to delete all non-numeric characters in re.sub, you should instead replace _all_ characters that could be a minus with '-' before doing the float(re.sub(...)) including the '-' instead of this bizarre ad-hoc code.Also "is_hyphen" is wrong since it doesn't handle the Unicode minus sign.
Bloombug red flag!!
PDFs don't use Unicode or ASCII codepoints, but Glyph ID used by fonts. Therefore all strings are converted to sequences of that Glyph ID. Original Unicode or ASCII texts are dropped, or can be linked and embedded for convenience. In many cases, a reverse conversion from ID to Unicode is silently done when text is copy-pasted or extracted from PDF.
That silent automatic reverse conversion tend to pick the numerically smallest Unicode codepoint assigned to the glyph(letter shapes), and many fonts reuses close-enough glyphs for obscure Unicode characters like ancient Chinese dictionary header symbols and dozen Unicode siblings of hyphens. Unicode also tends to have those esoteric symbols higher up in the table than commonly used ones.
Therefore, through conversion into Glyph ID and back into Unicode, some of simple characters like `角` or `-`, which glyphs tend to get reused to cover those technicalities, sometimes gets converted into those technicalities at remote ends.
1: https://en.wikipedia.org/wiki/Kangxi_radical
2: use TL: https://espresso3389.hatenablog.com/entry/20090526/124332747...
3: use TL: https://github.com/trueroad/tr-NTTtech05
4: use TL: https://anti-rugby.blogspot.com/2020/08/Computer001.html
Probably best to just transliterate to ASCII using gettext or unidecode or similar.
The reason why it is seldom used is that all keyboards by default provide only a way to easily type U+002D and the other ASCII characters. The input methods may provide some combination of keys that allows you to enter a minus or an en-dash, but nobody bothers to add an additional key combination for U+2010. The U+002D key could be reconfigured to output U+2010, but this would annoy the programmers who use programming languages where U+002D is used for minus.
So there is no way out of this mess. In programming languages or spreadsheets U+002D is used for minus, while in documents intended for reading, U+002D is used for hyphen, and the appropriate Unicode characters are used for minus and en-dash.
An exception among programming languages was COBOL. Originally it used only a hyphen character, which was used to improve the readability of long identifiers. This was possible because the arithmetic operations were written with words, i.e. SUBTRACT, so there was no need for a minus character.
A few years later (1964-12), when the PL/I language was developed to replace both FORTRAN and COBOL, they have introduced the underscore character, replacing the hyphen in long identifiers, where it was used to improve their readability like in COBOL, so that the hyphen/minus character could be used with the meaning of minus, like in FORTRAN. This convention has been inherited by most later programming languages, except by most dialects of LISP, which typically use a hyphen character in identifiers and they do not use a minus character, except for the sign of numbers.
The thing is, I've never found a single non-monospace typeface that displays U+002D as a minus sign or en-dash: it seems to be universally rendered shorter than a U+2212 or U+2013, whenever the latter have their own glyphs in the first place. I also did some testing on my system some time back, and 99% or more of typefaces treated a U+2010 identically to a U+002D. Only one or two displayed it a smidgeon shorter than a U+002D.
Hence my original question about whether it really is used for that purpose (or any other purpose) in practice.
Meanwhile, you do make a good point regarding programming languages. Though it would seem mostly coincidental to me that their use cases are almost always 'hyphen' or 'minus', as opposed to any of the other meanings of a 'typewriter dash'.
In Common Lisp, there is one character SP10 for Hyphen and Minus: https://www.lispworks.com/documentation/HyperSpec/Body/02_ac...
It is used
* in numbers as a sign -> -42
* in a conditional read macro as a minus operator -> #-ARM64(error "This is no 64bit ARM platform")
* in functions as a numeric minus operator -> (- 100 2/3) or (1- 100)
* as a global variable for the currently evaluated REPL expression -> -
* as a hyphen for symbols (incl. identifiers) -> UPDATE-INSTANCE-FOR-DIFFERENT-CLASS
- They will either use `--` or `---` as poor man’s en/em-dash or use the proper symbols
- They might use the proper minus sign but even if they don’t: post-processing can guess what is meant as “minus” in basic contexts (and even for math-heavy contexts: hyphens aren’t that common)
Furthermore hyphen-minus is rendered as a hyphen already. Not as minus or a dash.
It’s like a process of elimination: people who care already treat non-hyphens sufficiently different such that the usage of hyphen-minus is clear: it is just hyphen.
For me these things is mostly about looks and author-intent. Dashes look better than poor man’s dashes. Hyphen-minus looks like a hyphen minus already. And if I use hyphen-minus then I mean hyphen.
And for me it is less about using the correct character at the expense of possible inter-operation: the hyphen-minus is so widespread that I have no idea if 95% of software will even cope with using the real HYPHEN Unicode scalar. (I very much doubt that!)
The last thing is keyboard usability economics. I use en-dash/em-dash a few times per paragraph at most. Hyphens can occur several times a sentence. And since I need hyphen-minus as well (see previous point about interoperability) most keyboard setups will probably need to relegate it to some modifier keybind like AltGr-something… and no one has the patience for typing such a common symbol with a modifier combo.
From a stylistic perspective, it may have been desired for a given appearance even if technically wrong. Just because of a given typeface. I say this as someone who was an artist before learning software programming.
Or you could extend your editor to show Unicode character name in the status bar and do the inspection in a more immediate way
It's so messy that Unicode discourages use of APOSTROPHE for apostrophes, and instead recommends using RIGHT SINGLE QUOTATION MARK for apostrophes.
Blame fonts that render APOSTROPHE as a disgusting straight character.
(In my defence, I don't work in any of the specialized areas where it matters, and was raised in a poor, ascii only, western household.)
I will point out that spammers and scammers have been having a field day with this kind of character confusion for years now, and a lot of software still hasn't caught up to it.
On the bright side, the very old school database I babysit for work can be convinced to output utf8, including emoji, many of which render quite well in a terminal, allowing me to create bar graphs of poo or love heart characters, which honestly makes it all worth it for me.
I had huge amounts of rules for "unifying" unicode, so I could then run the result through various regular expressions. It wasn't just hyphens, but I'd run into all sorts of weird characters.
It all worked, but was very brittle, and constantly had to be tweaked.
In the end, I used a machine learning model, which I wrote about here[1]
[1] https://blog.line72.net/2024/07/31/the-joys-of-parsing-using...
Some characters are encoded differently based on what system set it. So an if statement character comparison runs into the same misery as the author has :(
I feel all "import data from multiple sources" I've seen in my life grew through repeated application of edge case handling.
I think the best solution would be to match by some specific code points, and then throw an error when a strange code point is encountered.
I think it’s a mistake to try to handle every edge case in this particular case.
Even some manpages had (or still do) have hyphen instead of minus for the option character. Argh!
This issue does indeed have a history of provoking unhinged lunacy. # converts idiotic number format containing random junk into normal
# represantion of a number
def junkno_to_normal(s):
(...)
There are so many random characters you can insert into dates or numbers. Not only hyphens but also all kind of white spaces or invisible characters. It's always a warm feeling when you import a text document and not only it's encoded in UTF-8 but you see YYYY-MM-DD date format. You know it's going to be safe from there. Unfortunately it's still very rare in my experience (even UTF-8 bit).Here's the first problem!
I can't believe actual businesses think that a report is anything other than for human eyes to look at. Typesetting (and report generation) is for presentation, and otherwise data should be treated like data.
I mean it's a different story if the product is like "we can help process your historical, improperly-formatted documents", but if it's from someone continually generating reports, somebody really should step in and make things more... computational.