26

What is a good way to indicate in programming that the sequence of digits you see is not a value you can do math on?

I use a lot of numbers which aren't "numbers". From account-numbers to patient-id numbers country codes to the codes for human sexes.

What I want to prevent is that others put those "numbers" into an integer data type.

Why? Because what might be a "number" now, may not be in the future: for instance the patient-id may now only contain numbers but we can have a situation where that changes to containing a Letter.

Also when you put "013" into an integer, you lose the leading 0 which leads to loss of information and subtle bugs.

How can I communicate this clearly?

14
  • 2
    Is there a specific language or paradigm that you are interested in? The language features that can be used address this vary quite a bit.
    – JimmyJames
    Commented Sep 7, 2023 at 16:39
  • 10
    Why? Because what might be a "number" now, may not be in the future. Start using characters and future (idiot) proof your design now.
    – S.D.
    Commented Sep 8, 2023 at 7:44
  • 1
    This is named Primitive Obsession. Interesting read: lostechies.com/jimmybogard/2007/12/03/…
    – Tvde1
    Commented Sep 8, 2023 at 9:06
  • 30
    If it's not a number, why are you making it a number and calling it a number? Call it a code or an ID or something else, and make it a String or other non-numeric type, and there should be little opportunity for confusion.
    – gidds
    Commented Sep 8, 2023 at 9:41
  • 14
    (Leading zeroes can have more drastic effects than that: e.g. in languages like C, a constant 013 gives the number 11, not 13!)
    – gidds
    Commented Sep 8, 2023 at 9:55

8 Answers 8

48

You can use a wrapper class/struct around your string to communicate the intent of the value. It will also allow you to validate inputs and achieve compile time safety, depending on the language you use. Or even completely hide the wrapped value. Here's a reasonable example on how to do it in C#

public class PrimaryAccountNumber
{
    private readonly string Value;

    public PrimaryAccountNumber(string value)
    {
        //validations
    }

    public void WriteTo(Stream stream)
    {
        //...
    }
   
}  

You can go further with inner classes, depending on what you want/need/are required.

public class PrimaryAccountNumber
{
    private readonly string WrappedValue;

    public PrimaryAccountNumber(string value)
    {
        //validations
    } 

    public class StreamPanWriter
    {
        private readonly Stream _stream;

        public StreamPanWriter(Stream stream)
        {
            _stream = stream;
        }

        public void WriteTo(PrimaryAccountNumber value)
        {                
            //the wrapped value is visible here
        }
    }       
}  
10
  • 1
    Exactly - creating Data-Types for these values also creates a perfect place to document details about the type, valid values/exceptions/caveats...
    – Falco
    Commented Sep 8, 2023 at 8:44
  • 9
    Depending on the language options a "wrapper type" can also be simply a type-alias with added validation.
    – Falco
    Commented Sep 8, 2023 at 8:46
  • 10
    Exactly; avoiding "primitive obsession" is an important concept that I wish more programmers knew about. It (a) prevents people doing maths on an ID number; (b) prevents a programmer from accidentally passing in a customer ID when they should have passed an order ID and (c) allows you to enforce domain rules in the constructor of the wrapper type, making it impossible to instantiate an invalid CustomerID and extinguishing what I call "validation anxiety"
    – Richiban
    Commented Sep 8, 2023 at 11:06
  • 7
    @GregBurghardt "They could simply change the wrapper class to use an int internally" the whole point of the wrapper class is that (a) no one outside the class cares what the internet representation is and (b) if necessary the wrapper class is, itself, tested
    – Richiban
    Commented Sep 8, 2023 at 15:46
  • 1
    Thanks for adding example code, my downvote just changed to an upvote! And good job NOT putting in a getter, which just ruins the whole point to hide the actual representation.
    – user949300
    Commented Sep 11, 2023 at 15:48
31

It sounds like you are already using string data types for these numbers (maybe in a data model or UML model or some code), but you want to prevent another dev to convert those strings accidentally to a number data type, because they ignored the original type or missed to read the documentation.

For this, my best recommendation is to write some automated tests (maybe integration tests) where you deliberately use some non-number values as test data. Design the tests in a way they will break when someone starts to use a number data type for processing those values.

And hope that other devs are smart enough to get the message and do not just "fix" the breaking tests by changing the test data.

2
  • 23
    Give the test a name that clearly indicates its intent. AccountNumber_Can_Contain_Alphanumerical_Characters
    – Rik D
    Commented Sep 7, 2023 at 15:43
  • 4
    And a comment describing a real-world example / region of a spec that proves that this is the case.
    – wizzwizz4
    Commented Sep 9, 2023 at 17:27
10

What you're dealing with here is a non-technical problem, it's a human one.

There are technical solutions to be found here, but they don't cover the problem in its entirety. To repurpose an old saying, the universe will eventually come up with a bigger idiot that foils your clever technical hurdle.

I'm apprehensive of solving this using a technical hurdle because your attempts at protecting yourself from bigger idiots will take more and more effort and time, on top of likely detracting from the readability and simplicity of your codebase.

So this answer is going to offer some lateral solutions:

Option A - Since you say that the ID content might change in the future to be non-numeric, why not get ahead of the curve and already turn it into a non-numeric value? It stops anyone with numeric intentions dead in their tracks. Even just adding an otherwise ignored prefix (turning 123 into A123) would suffice.

Option B - If you can't just use non-numerics in production right now (for whatever external reason), if you can control the testing layer you can still intentionally throw in some non-numeric values to test with and see if it breaks.

Option C - If your concern is that developers on your own codebase might make this mistake in the future, I'm worried about the team's trust in one another. If you know that this ID is non-numeric, so should the rest of the team. For trivial mistakes, there's the code review process to catch them. For endemic misconceptions, this seems like a team leadership or mentoring problem to solve - and if the issue is at this level, you're probably going to have more issues of a similar kind.

Option D - If your concern is that other dev teams working in other applications are going to misuse your identifier types, that's on them, not on you. You cannot control what others do, that's why they're a different team than you.
If you clearly document that this field can contain non-numeric characters, then they suffer the consequences of not reading your documentation when their numerical data type breaks once your ID values start changing.

Option E - I guess this is a somewhat technical one. People make bad calls because they're looking for the path of least resistance. If you make the correct path the path of least resistance, then you will find that people naturally gravitate towards doing things the way you want them done.
For example, you could provide a Nuget/npm/... package which provides all the data contracts and maybe even a neat and easy-to-use HTTP client. People will want to adopt it if it saves them the effort of having to do this work themselves, and you can sneakily make sure that they're using the contract the way you intended for it to be designed.
There's no guarantee that they will use your package (like I said before, you cannot control how others consume your service), but it will significantly mitigate the problem. The bigger the idiot, the more likely they will take the path of least resistance (blindly), which tends to mean that the worst offenders are the ones using your package.

5
  • 5
    You should try to remove the possibility of errors. For all the reasons you stated we could just stop using type systems, linters, automated code checks and so on. But we add all those layers because they prevent errors before they go into production. Dealing with this properly by using a wrapper class around the internal representation and leveraging the type system and constructor guarantees of the language similarly helps catching bugs before they go into production. People make mistakes, even good developers. Reducing the possible amount of mistakes is good.
    – Polygnome
    Commented Sep 8, 2023 at 0:08
  • @Polygnome: You can't guarantee that everyone will use your wrapper everywhere. At best, you can enforce it in your own code and interface, but not another dev's implementation. Nothing is stopping them from unwrapping or converting it. I'm not against the kinds of improvements you're suggesting by the way. I'm merely tempering OP's expectation as to how much they're realistically able to enforce its use by others. My issue isn't with the solution, it's with the (possible) endless pursuit of trying to fence in the behavior of another dev.
    – Flater
    Commented Sep 8, 2023 at 0:52
  • With enough ill- intent, you can destroy all safeguards in any system, obviuosly. You could Just use another language and write Garage directly into the Database. But thats Not why WE add the safeguards. WE add them Not for Security, but to reduce Bugs/Errors early. Well designed abstractions are easy to review. If the rule ist Always to use the wrapper, Reviews thats Spot Errors are trivial.
    – Polygnome
    Commented Sep 9, 2023 at 8:34
  • In case any other non-.NET developers are reading this: apparently ‘NuGet’ is a .NET package manager. (Other package managers — and languages — are available…)
    – gidds
    Commented Sep 9, 2023 at 18:55
  • @gidds Mentioned npm as well in the interest of not picking a side there.
    – Flater
    Commented Sep 10, 2023 at 10:04
4

Use a string datatype, and maybe communicate permissible values in the documentation using regular expressions.

4
  • 5
    Also, spell the string values in ways that make this obvious. Rather than "0049405554236", write phone numbers "+49 40 555 4236", and credit card numbers as "1512 8245 5240 5668", etc. Commented Sep 7, 2023 at 15:24
  • 2
    OP's question isn't so much on what datatype they should be using, but rather how to prevent others from easily converting it to an integer data type. See: "What I want to prevent is that others put those "numbers" into an integer data type"
    – Flater
    Commented Sep 7, 2023 at 22:52
  • 4
    You can't prevent others from being stupid. Has been tried often, never really worked. But clear documentation can at least help those who don't insist on being stupid. Commented Sep 8, 2023 at 5:45
  • "You can't prevent others from being stupid" and in this case the OP is worried about accidental saboteurs. There's absolutely no reason why someone should be motivated to go through the trouble of changing a correctly defined non-numeric type to a numeric type. There's not much you can do to pre-emptively defend against such malevolent stupidity.
    – JimmyJames
    Commented Sep 8, 2023 at 18:58
2

What you have are identifiers that just happen to be numeric. If you type them as identifiers, not numbers, then nobody is ever going to confuse the two.

There is of course no way to prevent developers from intentionally misusing your identifier as a number, but given that you've honestly done your best to prevent such misuse, these cases simply aren't your problem. A bad developer will always find a way to break the most idiot-proof code; you should write your code to be used by people with common sense, not idiots.

2

For example, create a class PhoneNumber and a class CreditCardNumber (careful about storing the latter), obviously without any method to turn it into a large integer. That prevents people from doing a lot of stupid things.

You would at best have methods to create a string representing the object, possibly in multiple formats, and locale specific. For example you display a phone number with or without country code depending on whether the country code matches the user’s country. Or you might have spaces between groups of digits in a credit card number. And a tiny image of the credit card might be nice.

If another developer is stupid enough to extract the digits from a phone number, drop a leading “+” and/or zero, that’s not your problem.

1

I use a lot of numbers which aren't "numbers". From account-numbers to patient-id numbers country codes to the codes for human sexes.

Strictly speaking, Users can't work directly with Numbers. Or Dates.
Not what a Computer or a Database considers to be a Number or a Date, anyway.

Such data gets stored according to IEEE specifications, of which Users know nothing and haven't been able to enter into machine memory locations since we used mechanical switches on physical machines to do so.

Users always work with a Character Representation of Data. The sort of thing that they can type from their keyboard.

Any data that comes into your Application / Database needs to be stored in the appropriate Data Type.

  • If it's a [countable] number, then it should go into a numeric Data Type.
  • If it's a date value, then it goes into a Date Data Type.
  • Pretty much everything else goes into a Character Data Type.

This "inwards" formatting for storage eliminates your "loss of information and subtle bugs". The converse, "outward" formatting for display, makes sure that the data is presented to the User in a form that they are comfortable with (UTC datetimes might be great for Applications, but not many people choose to work with them!)

If Developers choose to write SQL that compares numeric and character data, then they will have to explain and/or correct the abysmally poor performance that they get from such queries, or the strange results they get, with [character] data being "incompletely converted" into numbers by some DBMSes.

0

Write a random generator for each class of such values (making sure that it can at least in theory output every valid value), then use the generators everywhere you need an arbitrary such value in tests. This

  • ensures tests fuzz any code which deals with such values,
  • clarifies in which tests the actual values are relevant (they are fixed rather than random), and
  • if anyone tries changing a generator this should stand out very explicitly during code reviews.

For example, you could have a random credit card number generator. It should return a string (if your language even distinguishes them from numbers in a useful way) with a random valid value. If someone for example changes the code to return a number it'll probably break tests whenever the value is less than 1000_0000_0000_0000, since any such integer, when treated as a string anywhere in your code, will have the wrong length.

For a bunch of examples, see these tests.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.