<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Peter</title>
    <description>The latest articles on DEV Community by Peter (@redpoint9).</description>
    <link>https://dev.to/redpoint9</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4087005%2Fd8cee0b5-1d66-4650-859c-0ee63f12e0ed.png</url>
      <title>DEV Community: Peter</title>
      <link>https://dev.to/redpoint9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/redpoint9"/>
    <language>en</language>
    <item>
      <title>One invisible character broke skin tones for 12 emoji, and I shipped it</title>
      <dc:creator>Peter</dc:creator>
      <pubDate>Fri, 21 Aug 2026 07:33:59 +0000</pubDate>
      <link>https://dev.to/redpoint9/one-invisible-character-broke-skin-tones-for-12-emoji-and-i-shipped-it-3mbg</link>
      <guid>https://dev.to/redpoint9/one-invisible-character-broke-skin-tones-for-12-emoji-and-i-shipped-it-3mbg</guid>
      <description>&lt;p&gt;&lt;code&gt;👍&lt;/code&gt; supports five skin tones. My emoji API said it supported none.&lt;/p&gt;

&lt;p&gt;The data was right. Both upstream sources were right. The bug was one line of lookup code and a character you cannot see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;I had just published an emoji API and was checking endpoints by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://eeemoji.com/api/v1/emojis/thumbs-up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"emoji"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"👍"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"thumbs up"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"skinToneSupport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"skinToneVariants"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is plainly wrong. Thumbs up has had skin tone variants since Emoji 1.0.&lt;/p&gt;

&lt;p&gt;The tempting move here is to patch the response. Hard-code a list of hand emoji, set the flag, move on. I nearly did. Instead I went looking for why, and the why turned out to affect 152 records, not the ten or so I had spotted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two sources, both correct
&lt;/h2&gt;

&lt;p&gt;My data bundle is generated from two packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/milesj/emojibase" rel="noopener noreferrer"&gt;&lt;code&gt;emojibase-data&lt;/code&gt;&lt;/a&gt; for names, keywords, shortcodes and skin tone variants&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/muan/unicode-emoji-json" rel="noopener noreferrer"&gt;&lt;code&gt;unicode-emoji-json&lt;/code&gt;&lt;/a&gt; for the &lt;code&gt;skin_tone_support&lt;/code&gt; flag&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First thought: they disagree, and I picked the wrong one. So I checked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thumbs up      emojibase skins=5    unicode-emoji-json skin_tone_support=True
thumbs down    emojibase skins=5    unicode-emoji-json skin_tone_support=True
raised hand    emojibase skins=5    unicode-emoji-json skin_tone_support=True
raised fist    emojibase skins=5    unicode-emoji-json skin_tone_support=True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sources say yes. Neither is wrong. My generator was producing &lt;code&gt;false&lt;/code&gt; from two inputs that both said &lt;code&gt;true&lt;/code&gt;, which is the sort of result that means you are looking at the wrong layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual bug
&lt;/h2&gt;

&lt;p&gt;Here is the line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;skinSupport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;unicodeEmojiData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;eb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;skin_tone_support&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;eb.emoji&lt;/code&gt; is emojibase's emoji character. &lt;code&gt;unicodeEmojiData&lt;/code&gt; is keyed by emoji character. This looks like it cannot fail.&lt;/p&gt;

&lt;p&gt;It fails for 152 of 1,949 emoji, because the two packages disagree about something invisible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// emojibase's `emoji` field for thumbs up&lt;/span&gt;
&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;eb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;codePointAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; ['1f44d', 'fe0f']     &amp;lt;-- includes U+FE0F&lt;/span&gt;

&lt;span class="c1"&gt;// unicode-emoji-json's key for the same emoji&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; '1f44d'               &amp;lt;-- no U+FE0F&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;U+FE0F is the &lt;strong&gt;variation selector&lt;/strong&gt;. It is a zero-width character that tells the renderer "draw this as colour emoji, not monochrome text". It has no visual form of its own. Both strings print as 👍 and look identical in every editor, diff and terminal I use.&lt;/p&gt;

&lt;p&gt;So the lookup missed. And then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;swallowed the miss and returned a confident, wrong answer. No exception, no warning, no undefined creeping into the output. Just &lt;code&gt;false&lt;/code&gt;, in 152 records, shipped to production and served over a public API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the fallback is the real villain
&lt;/h2&gt;

&lt;p&gt;The FE0F mismatch is a fact of life. Emoji data packages disagree about variation selectors constantly, and both are defensible: emojibase gives you the fully-qualified sequence, unicode-emoji-json gives you the base.&lt;/p&gt;

&lt;p&gt;The thing that turned a mismatch into a bug is &lt;code&gt;?? false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;??&lt;/code&gt; on a lookup you expect to succeed converts "I could not find this" into "the answer is no". Those are completely different statements, and the second one is unfalsifiable from the output. If the code had thrown, or produced &lt;code&gt;undefined&lt;/code&gt;, or logged once, I would have caught it in minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// what I wrote&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;skinSupport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;skin_tone_support&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// what would have caught it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;stripVariationSelectors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`No entry for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;toCodePoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;skinSupport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;skin_tone_support&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule I took from this: &lt;strong&gt;a &lt;code&gt;??&lt;/code&gt; default on a lookup that should always hit is a silent-failure generator.&lt;/strong&gt; Use it for genuinely optional data. For a required join, fail loudly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it against the actual authority
&lt;/h2&gt;

&lt;p&gt;Rather than patch the generator and re-run it, I reconciled the whole bundle against Unicode's own &lt;a href="https://unicode.org/Public/emoji/latest/emoji-test.txt" rel="noopener noreferrer"&gt;&lt;code&gt;emoji-test.txt&lt;/code&gt;&lt;/a&gt;, which lists every fully-qualified sequence including toned ones. If the file contains &lt;code&gt;1F44D 1F3FB&lt;/code&gt;, thumbs up supports skin tones. That is the authority, not a package's interpretation of it.&lt;/p&gt;

&lt;p&gt;The script derives both the flag and the variant list, so it can be re-run when Unicode ships a new version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; emoji-test.txt https://unicode.org/Public/emoji/latest/emoji-test.txt
&lt;span class="nv"&gt;EMOJI_TEST_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;emoji-test.txt node scripts/fix-skin-tone-data.js &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result: 26 records corrected, 311 to 330 with skin tone support.&lt;/strong&gt; Thumbs up and down, raised hand, raised fist, the four pointing hands, ear, snowboarder, surfer, swimmer, wrestlers, bunny ears, ballet dancer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three bugs in the fix itself
&lt;/h2&gt;

&lt;p&gt;I wrote &lt;code&gt;--check&lt;/code&gt; mode before &lt;code&gt;--write&lt;/code&gt; mode, which is the only reason this section exists rather than a story about corrupting 1,900 records.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Stripping every FE0F.&lt;/strong&gt; My first attempt removed all variation selectors before looking up the toned sequence. But the toned form of 👱‍♀️ is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1F471 1F3FB 200D 2640 FE0F
      ^tone            ^this one survives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the selector attached to the &lt;em&gt;base&lt;/em&gt; is replaced by the modifier. Later ones belong to other parts of the sequence. Stripping all of them made &lt;strong&gt;128 emoji look like they had lost skin tone support&lt;/strong&gt;. Caught because 👱‍♀️ obviously has tones, so the rule had to be wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Multi-person emoji tone every figure.&lt;/strong&gt; 🧑‍🤝‍🧑 is not &lt;code&gt;person + tone + handshake + person&lt;/code&gt;. It is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1F9D1 tone 200D 1F91D 200D 1F9D1 tone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both people. My bundle had stored only the first figure toned, which renders as a toned person beside an untoned one. The flag was right and the &lt;em&gt;variants&lt;/em&gt; were wrong, so the fix now checks those independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The fix for #2 matched everything.&lt;/strong&gt; My "tone every person code point" function returns the sequence unchanged when there are no person code points. An unchanged sequence is of course present in the file, so every emoji "passed" and the script proposed &lt;strong&gt;1,376 gains including 😀 grinning face&lt;/strong&gt;. Fixed by requiring that a modifier was actually inserted.&lt;/p&gt;

&lt;p&gt;Each of those passed typecheck and lint. None would have been caught by anything except running the thing and reading the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell past me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;When output contradicts every input, the bug is in the join, not the data.&lt;/strong&gt; I spent time diffing two sources that agreed with each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;?? false&lt;/code&gt; on a required lookup is a lie generator.&lt;/strong&gt; It answers a question you did not ask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invisible characters need a code point view.&lt;/strong&gt; &lt;code&gt;console.log(emoji)&lt;/code&gt; shows you nothing. &lt;code&gt;[...s].map(c =&amp;gt; c.codePointAt(0).toString(16))&lt;/code&gt; shows you the bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write &lt;code&gt;--check&lt;/code&gt; before &lt;code&gt;--write&lt;/code&gt;.&lt;/strong&gt; My data-fix script had three bugs. Dry run caught all three.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;The API this came out of is at &lt;a href="https://eeemoji.com/api" rel="noopener noreferrer"&gt;eeemoji.com/api&lt;/a&gt;. Free, no key, and the skin tone flags are now reconciled against Unicode 17.0. If you are pulling emoji data from more than one package, it is worth checking how many of your joins are quietly falling through to a default.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>unicode</category>
      <category>emoji</category>
      <category>debugging</category>
    </item>
    <item>
      <title>The barcode prefix does not tell you where a product was made</title>
      <dc:creator>Peter</dc:creator>
      <pubDate>Thu, 20 Aug 2026 16:52:54 +0000</pubDate>
      <link>https://dev.to/redpoint9/the-barcode-prefix-does-not-tell-you-where-a-product-was-made-3ing</link>
      <guid>https://dev.to/redpoint9/the-barcode-prefix-does-not-tell-you-where-a-product-was-made-3ing</guid>
      <description>&lt;p&gt;There is a claim about barcodes that circulates every few years, usually attached to a call to boycott something: the first digits tell you which country a product was made in. Scan a jar, read the prefix, learn the origin.&lt;/p&gt;

&lt;p&gt;It is wrong, and the organisation that issues the numbers says so directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Since GS1 user companies can manufacture products anywhere in the world, GS1&lt;br&gt;
Prefixes do not identify the country of origin for a given product.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What the prefix actually identifies is &lt;strong&gt;the GS1 member organisation that allocated the number to the brand owner&lt;/strong&gt;. A company registers with one organisation, usually the one where it has its seat, and keeps those number ranges wherever it later manufactures. A Swedish company with a &lt;code&gt;73x&lt;/code&gt; prefix can have every unit made in Vietnam and the barcode will not change.&lt;/p&gt;

&lt;p&gt;I went looking for a dataset that got this right and did not find one, so I built it. It sits behind &lt;a href="https://smartatest.se/guider/ean-landsprefix" rel="noopener noreferrer"&gt;a Swedish-language lookup tool&lt;/a&gt; where you can paste a barcode and see the same answer without installing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What annoyed me enough to build something
&lt;/h2&gt;

&lt;p&gt;Most lookup services label the field &lt;code&gt;country of origin&lt;/code&gt;. Not &lt;code&gt;issuing organisation&lt;/code&gt;, not &lt;code&gt;country of the issuing organisation&lt;/code&gt;. The label itself teaches the myth to everyone who uses the tool.&lt;/p&gt;

&lt;p&gt;The npm package with the most obvious name is &lt;code&gt;gs1-prefix-code-to-country-code&lt;/code&gt;. Last published in 2023, no TypeScript types, no keywords, and the entire API is named after the wrong idea. Install it and the mistake propagates into your codebase as a function name.&lt;/p&gt;

&lt;p&gt;That felt like a small thing worth fixing properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two sources, and what happens when they disagree
&lt;/h2&gt;

&lt;p&gt;The table is reconciled against GS1's own prefix list and the English Wikipedia article &lt;code&gt;List of GS1 country codes&lt;/code&gt;. Most entries agree. Two do not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;612&lt;/code&gt; appears in Wikipedia as Somalia, but is absent from GS1's own list&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;894&lt;/code&gt; is listed as Bangladesh by Wikipedia, while GS1 says the range is
managed centrally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The obvious move is to pick one and move on. I kept both disagreements in the record instead, as a boolean field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GS1_RANGES&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gs1-prefix&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;GS1_RANGES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceDisagreement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// the two ranges where the sources conflict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part I actually care about. A dataset that silently resolves conflicts looks cleaner and tells you less. If you are building something where an unallocated prefix matters, you want to know that one of your 144 ranges rests on a single source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;gs1-prefix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lookup&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gs1-prefix&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;7310865004703&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                        &lt;span class="c1"&gt;// "731"&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuingOrganisation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// "GS1 Sweden"&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organisationCountry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// "Sweden"&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organisationCountryCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "SE"&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkDigitValid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python, same data, same names in snake_case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;gs1-prefix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;gs1_prefix&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lookup&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7310865004703&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;issuing_organisation&lt;/span&gt;   &lt;span class="c1"&gt;# "GS1 Sweden"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or skip the dependency and take the CSV, JSON or DCAT-AP file from &lt;a href="https://gs1-prefix-ranges.vercel.app" rel="noopener noreferrer"&gt;gs1-prefix-ranges.vercel.app&lt;/a&gt;. CORS is open, so you can fetch it straight from a page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detail that trips people up
&lt;/h2&gt;

&lt;p&gt;A 12-digit UPC-A is a GTIN-13 with an implied leading zero. Read the twelve digits as they are and you land on the wrong prefix, and the answer still looks plausible, which is the worst kind of bug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;012345678905&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;paddedFromUpc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;012345678905&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// "001", not "012"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flag is there so you can tell the two cases apart rather than wondering.&lt;/p&gt;

&lt;p&gt;The check digit follows the same principle. It returns &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;, or &lt;code&gt;null&lt;/code&gt; when the length carries no check digit, so "there is no check digit here" and "the check digit is wrong" are different answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ranges that are not countries at all
&lt;/h2&gt;

&lt;p&gt;Worth knowing before you write &lt;code&gt;range.organisationCountry&lt;/code&gt; into a UI:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;020&lt;/code&gt;-&lt;code&gt;029&lt;/code&gt;, &lt;code&gt;200&lt;/code&gt;-&lt;code&gt;299&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Restricted distribution, defined locally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;040&lt;/code&gt;-&lt;code&gt;049&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Restricted to within one company&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;977&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Periodicals, ISSN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;978&lt;/code&gt;-&lt;code&gt;979&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Books, ISBN. &lt;code&gt;979-0&lt;/code&gt; is sheet music, ISMN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;980&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Refund receipts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;981&lt;/code&gt;-&lt;code&gt;983&lt;/code&gt;, &lt;code&gt;990&lt;/code&gt;-&lt;code&gt;999&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Coupons&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those carry &lt;code&gt;issuingOrganisation: null&lt;/code&gt; and a &lt;code&gt;note&lt;/code&gt; explaining what the range is for. Around 281 of the 1000 possible prefixes are unallocated, and the lookup returns a null range rather than guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  One caution if you maintain your own copy
&lt;/h2&gt;

&lt;p&gt;Several GS1 member organisations publish stale versions of the list. GS1 Slovakia's still names Serbia and Montenegro, a country that stopped existing in&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A stale list looks exactly like a current one, so check the date on
whatever you copy from.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That applies to this dataset too. It is generated from a table I maintain, and the generation date is in the JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Licence and where it lives
&lt;/h2&gt;

&lt;p&gt;MIT for the code, CC BY 4.0 for the data. Attribution goes to &lt;a href="https://smartatest.se/guider/ean-landsprefix" rel="noopener noreferrer"&gt;smartatest.se&lt;/a&gt;, where the table is maintained.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site and raw files: &lt;a href="https://gs1-prefix-ranges.vercel.app" rel="noopener noreferrer"&gt;gs1-prefix-ranges.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/xoocode/gs1-prefix-ranges" rel="noopener noreferrer"&gt;github.com/xoocode/gs1-prefix-ranges&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm and PyPI: &lt;code&gt;gs1-prefix&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Corrections to the data are welcome as an issue. If you find a range where GS1 and Wikipedia disagree and I have not flagged it, that is the most useful thing you could tell me.&lt;/p&gt;

</description>
      <category>watercooler</category>
    </item>
  </channel>
</rss>
