A critique of NHS Number implementations

Email Harold Thimbleby :: October 2022

This web site has examples that show the very unprofessional state of NHS Digital’s implementing even simple things like NHS numbers. We focus on programming user interfaces for entering NHS numbers.

NHS Digital has some sample user interfaces, including one for entering NHS numbers.

It’s unhelpful just to criticise, so I wrote a NHS Number user interface: it allows a user to enter an NHS number, and you can click through to it from this page. It wasn’t at all difficult — it took me just over two hours to write the first version, plus a day iterating the design and improving details. It took about two hours to write and review the documentation you can read below.


Basic documentation for an NHS number entry user interface

How this interactive documentation works

This interactive documentation you are reading was generated automatically from the NHS number user interface (which you can ), so the code shown here is exactly what is used in that demonstration.

The idea is that documentation and code can be very easily interactively hidden or revealed by clicking. You can control exactly what you want to read without being distracted by lots of detail.

Anything — whether code, comments or HTML — that isn’t relevant to explaining the code is initially hidden, to make the code easier to read and understand. You can then look at and explore exactly what you want to see, and in whatever order you like.

You can click on any hidden code or doccumentation to reveal it or to hide it again, or you can press the open all/close all buttons — — to see (or conceal) everything.

Obviously, the exact structure of the documentation (and its initial state) depends on design choices that the developers made. Basically, the documentation works like this: the developer wrote comments in the usual way in the program, and the automatic documentation tool converted the comments into this easier to read interective form, which you are reading now.

If you like, click to run the NHS number interface, then use your browser to open the source file (you probably need to be in developer mode). You’ll see all this interactive documentation embedded in ordinary comments in the HTML and in the source code — the documented source code works directly without anything special needing to be done.

The end of this web page has a brief technical description of all the features of the documentation tool and how it works. It’s really very simple.

HTML files start in a standard way, which is hidden here
<!DOCTYPE html> <html> <head>     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <meta name="author" content="Harold Thimbleby" />     <meta name="version" content="v1 (8 Nov 2019)" />     <title>NHS Number</title>     <style>         .blink {             animation: blinker .125s step-start infinite;             color: orange;         }         @keyframes blinker {             50% {                 opacity: 0;             }         }         .tick {             color: green;         }         body {             font-family: "Helvetica";             background-color: #F0F0F0;         }     </style> </head> <body>
The HTML body
Main code, between <script>...</script>
    <script>
Define possible separators
        var sepSpaces = "&nbsp;";         var sepDash = "&ndash;";         var separator = sepSpaces; // initially spaces 
We have since discovered that NHS Numbers can only use spaces as separators. The program no longer supports dashes as an alternative, but warns the user that this option is not allowed.
        if (separator != sepSpaces)             alert("*** Code has been modified incorrectly. Please check source code and comments!");
Function insertSeparators() inserts separators in the right places in the NHS number string
example: given 485773456 return 485 777 3456 since we have separate head and tail strings, the offset may vary. the two cases (used in the program) are: insertSeparators(0, buf.hd) -- the lefthand buffer has 0 offset insertSeparators(buf.hd.length, buf.tl) --- the righthand buffer is offset by the length of the lefthand buffer
        function insertSeparators(offset, n) {             var s = "";             for (var i = 0; i < n.length; i++)                 s += (i + offset == 3 || i + offset == 6 ? separator : "") + n[i];             return s;         }
Check insertSeparators() works - try one test (I tried others but I've only hard-coded one in this code :-)
        var input = "1234567890";         var test = insertSeparators(0, input);         if (test.split(separator).join("") != input)             alert("Digits are changed by pad: from " + input + " to " + test + "\nOR NOT JOINED CORRECTLY? " + test.split(separator).join()); // Now the test works we don't need to say...  // else alert("passes test :-)"); 
Function to generate warning sound
        function beep() {
Lots of sound data to make a beepy sound
            var snd = new Audio("data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");
            snd.play();         }
Function to check the checkdigit is correct
        function checkDigit(n) {             if (typeof n != "string")                 alert("type error! checkDigit(" + n + ")");             if (n.length < 10) return "too short";             var weightedSum = 0;             for (var i = 0; i < 9; i++) {                 var factor = 10 - i; // check it works....                  if (typeof n[i] != "string" && n[i].length != 1)                     alert("digit type error! n[" + i + "] is " + typeof n[i] + "\n'" + n + "'\nn[i].length=" + n[i].length);                 if (n[i] < 0 || n[i] > 9)                     alert("digit " + (i + 1) + " (n[" + i + "]) out of range! " + n[i]);                 weightedSum += factor * n[i];             }             var check = 11 - weightedSum % 11; // check should be in range 0-10, and of course 10 is not a digit              if (check < 0 || 9 < check) return "bad";             return n[9] == check ? "good" : "bad";         }
Check it works on my own NHS number
        if (checkDigit("4465351798") != "good")             alert("checkDigit() doesn't work yet");
The user interface buffer
The buffer is split into head and tail strings, abbreviated hd and tl. The cursor is between them.
        var buf = {             hd: "", // lefthand part of buffer (head)              tl: "" // righthand part of buffer (tail)          };
Function display() to display the buffer
        function display() {             var leng = buf.hd.length + buf.tl.length;             var cd = checkDigit(buf.hd + buf.tl);             var marker = cd == "good" ? "<b class=tick>&nbsp;&#9989;&nbsp;</b>" :                 cd == "bad" ? "&nbsp;<b style='color:red'>X</b>&nbsp;" : "";             marker = "&nbsp;" + marker + "&nbsp;";             if (leng == 0) {                 document.getElementById("result").innerHTML = "&nbsp;<span style=color:silver>Type an NHS number here</span>";                 document.getElementById("message").innerHTML = "";             } else { // use insertSeparators() to insert separators in correct places                  document.getElementById("result").innerHTML = "&nbsp;" + marker + insertSeparators(0, buf.hd) + "<span class=blink>'</span>" + insertSeparators(buf.hd.length, buf.tl) + marker;                 document.getElementById("message").innerHTML = cd == "good" ? "This NHS number is OK<p><b>Note: as this is a demo, it has not been checked whether the NHS number &ldquo;" + insertSeparators(0, buf.hd) + insertSeparators(buf.hd.length, buf.tl) + "&rdquo; corresponds to any real patient number.</b>" :                     cd == "bad" ? "The NHS number has been mistyped &mdash; please double-check it" : "The NHS number being entered is not complete";             }         }
Keystroke event listener to handle key input and update the display buffer
        document.addEventListener('keydown', function (event) {             switch (event.keyCode) {             case 8: // delete - remove rightmost character in head (left) buffer                  if (buf.hd.length <= 0) beep();                 else buf.hd = buf.hd.slice(0, -1);                 break;             case 39: // right                  buf.hd = buf.hd + buf.tl.slice(0, 1);                 buf.tl = buf.tl.slice(1);                 break;             case 38: // up - move cursor to far right                  buf.hd = buf.hd + buf.tl;                 buf.tl = "";                 break;             case 40: // down - move cursor to far left                  buf.tl = buf.hd + buf.tl;                 buf.hd = "";                 break;             case 37: // left                  buf.tl = buf.hd.slice(-1) + buf.tl;                 buf.hd = buf.hd.slice(0, -1);                 break;
Handle digits 0-9
            case 48:             case 49:             case 50:             case 51:             case 52:             case 53:             case 54:             case 55:             case 56:             case 57: // put digit into the head buffer                  if (buf.hd.length + buf.tl.length < 10) // each digit is a string, namely the Unicode character of the digit                      buf.hd += "" + (event.keyCode - 48); // JavaScript idiom to convert a number into a string                  else beep();                 break;
Change separator — handle space and dash separators
// The user interface allows space and dash to be typed anywhere within an NHS Number. They convert the display format according to the user's preference. (This works as the space/dash separator is provided by the program, not by the user.)  // The user interface has been modified according to conform to standard ISB 0149 NHS Number and now only permits spaces as separators. 
            case 32: // space                  separator = sepSpaces;                 break;             case 189: // dash                  if (true) // make false to support space/dash separators                      alert("You typed a dash in an NHS Number. Dashes are not allowed. Your dash has been ignored, and a space will be used as a separator for the NHS Number you are entering.");                 else                     separator = sepDash;                 break; // @end-collapsse 
Handle anything else - it's an error
            default: // unrecognised keystroke of some sort, so beep to warn the user                  beep(); // alert("missed = "+event.keyCode); // to debug                  return; // return immediately and don't update display 
            }             display(); // update the display (unless there was an error (where we beeped)          }, true);
On iPads and iPhones there is some tedious work to make sure the keyboard is displayed when the user clicks in the input field (which has ID 'result')
        function getKeyboardFocus() { // ensure iOS displays a keyboard...              var inputElement = document.getElementById('hiddenInput');             inputElement.style.visibility = 'visible'; // unhide the input              inputElement.focus(); // focus on it so keyboard pops              inputElement.style.visibility = 'hidden'; // hide it again              display(); // get rid of the unwanted cursor          }
    </script>
NHS number user interface and explanations
    <h1>NHS number user interface demonstrator</h1>     <table>         <tr>             <td>The NHS number is a 10 digit number,</td>             <td>like</td>             <td>485 777 3457, which is a valid NHS number,</td>         </tr>         <tr>             <td></td>             <td align=right>or                 <td>485 777 3456, which is an invalid NHS number.</td>         </tr>     </table>     <p>         You should be able to find your NHS number on any letter the NHS has sent you, on a prescription or by logging into your GP practice online service.     </p>
The NHS number buffer, displayed in HTML
    <tt>         <div id="result" onkeydown="handle(1)" onmouseup="getKeyboardFocus()" style="padding:1.5mm;background:LightCyan; border: solid; font-size: 16pt;">         </div>     </tt>
Display error messages, if any
    <font>         <div style="padding:1.5mm;padding-top:3mm" id="message">   </div>          <input id="hiddenInput" style="visibility: hidden;">     </font>
Display the user interface buffer
    <script>         display();     </script>
Final instructions and useful links
    <div style="border:solid 1px;border-radius: 10px;padding:4px;background-color:lightgray">         <h2>Useful links</h2>         <ul>             <li>                 <p>                     <a href="http://www.harold.thimbleby.net/NHSnumbers.pdf">A peer reviewed paper discussing NHS numbers and user interfaces</a>                     <br/>Cite as: Harold Thimbleby. 2022. &ldquo;NHS Number open source software: Implications for digital health regulation and development.&rdquo; <em>ACM Transactions on Computing for Healthcare</em> X, X, Article X (in press), 29 pages. <a href="https://doi.org/10. 1145/3538382">DOI 10. 1145/3538382</a>                 </p>             </li>             <li>                 <p><a href="https://service-manual.nhs.uk/design-example/components/text-input/number?fullpage=undefined&blankpage=undefined">NHS example for entering an NHS Number</a>. Try entering anything &mdash; like text &mdash; and see how errors are ignored.</p>             </li>             <li>                 <p>That NHS example can be found in this longer document:                     <a href="https://service-manual.nhs.uk/design-system/components/text-input">NHS's own discussion of text input, including NHS Digital&rsquo;s NHS number example</a>, which this demonstration has copied its appearance from (though it does interaction and error handling more sensibly...)                     <p>NB the original web site referenced in the paper was <a href="https://beta.nhs.uk/service-manual/styles-components-patterns/text-input">beta.nhs.uk/service-manual/styles-components-patterns/text-input</a> which has since disappeared.                     </p>             </li>             <li>                 <p>                     <a href="https://digital.nhs.uk/services/nhs-number/nhs-number-client">NHS's overview of the format of an NHS Number and their Java NHS Number client</a>                 </p>             </li>             <li>                 <p>                     <a href="documentation.html">Interactive documentation for the code implementing the user interface demonstrated here</a> (Note that this file includes documentation of how the documentation itself works.)                 </p>             </li>             <li>                 <p>                     <a href="documentation-structure.html">How the documentation above is mixed into the strucutre of the nested code, making it easy to write and easy to ensure correct</a> (It is an automatically-generated overview file.)                 </p>             </li>             <li>                 <p>                     <a href="http://www.harold.thimbleby.net">Harold Thimbleby&lsquo;s web site</a>                 </p>             </li>         </ul>     </div>
Final bits to finish off the HTML file
</body> </html>

How does the documentation tool work?

This documentation tool (version 1.1) reads any file in any language. Currently, the documentation tool recognizes strings and comments.

Strings are sequences of characters between matched ", ' or `. As usual, within a string, the string delimiter itself can be escaped with \

Comments are standard C or HTML style comments:

  • // to the end of the line
  • <! to >
  • /* to */
  • Comments inside comments are not recognized, so, as usual, comments cannot be nested.
  • Comments inside strings are not recognized — something like "http://doi.org" is just a string, even though it contains the comment symbol //

All comments (outside of strings) are converted to blue boxes, or to gray boxes if the comment contains @collapse

A comment may contain keywords:

  • @title text ↵ — Define the title for the generated documentation. You only require one of these — it defines the HTML <title> text for the generated documentation.

    Where ↵ means a newline or the end of the comment.

  • @collapse text ↵ — everything to the next comment containing a matching @end-collapse is collapsed. You can nest collapsed text.

    You can have comment text before or after these @collapse and @end-collapse directives, and even more than one directive in one comment if you wish. (It’s often neater to have several @end-collapse directives in a single comment.) Any text between the directives is, of course, treated like comment.

  • @buttons — the open all/close all buttons are inserted, like this: (@buttons was used in this comment to show how it works).
  • @move-up — move this comment to before the preceding code in the generated documentation.

    This strange feature is required because HTML files must always start <!DOCTYPE html> ... specifying language and character sets, etc. HTML files cannot start with comment, but when a HTML file is documented, you may want the essential code moved below your own introduction (which needs to be written in a comment), for instance to give the document a title. Code moved after a comment can also be collapsed and hidden if the comment contains a @collapse.

  • @move-down — conversely, files may need to end with special code, so this allows the documentation to end with arbitrary comment. Since everybody ignores what comes after </html> I haven’t thought of a good use for this yet, but it is an obvious match for @move-up!
  • @documentation — insert this documentation text.

    The @documentation feature might seem quirky, but originally I wrote the explanation of how the tool works in code I was documenting with the tool (so it looked like the explanation you are reading right now). But of course, I also needed the same documentation for the tool itself!

    To avoid having two independent descriptions of how the tool works to keep synchronized, I put all the documentation inside the tool, where it properly belongs.

    Now that this documentation text is inside the tool, by using @documentation the one-and-only-documentation can be repeated anywhere it’s needed. I can now have lots of independent copies of this documentation text, but I’ve only got to maintain one of them, and all the uses of the documentation are kept up to date automatically every time the tool is run.

    Now the documentation is inside the code that implements it, which is the best place for it to be — if I edit the documentation to make it better, I can easily improve the adjacent code to implement the new documentation; conversely, if I modify the code, I can very easily keep its documentation up to date.

  • @instructions — inserts brief instructions for using the interactive documentation generated by the tool.

    The @instructions feature is provided in case the documentation tool is modified and introduces new or different interactive features. Like @documentation (described above), making @instructions a documentation tool function keeps all the information about using the documentation generated by the tool in one place so it’s easier to maintain and keep correct.

  • @tabs n ↵ — Set the tab size to n instead of the default 8. (Conventionally tabs align to the next multiple of 8 columns, but some code editors let you change it.) You can set tabs at most once in a document.

All the formatting is done using CSS, and the interactivity is done by JavaScript. You can edit both to get different effects.

Finally, because the open/close outlining is interactive, I decided that there is no feature to hide code from sight. You can’t cheat.

That’s it.


Documentation tool used version 1.1
Documentation generated at 10:59 pm, 11 October 2022