UFI (Unique Formula Identifier)

Date: 2020-09-29

Base source: https://github.com/PetrPodrazil/UFI

https://poisoncentres.echa.europa.eu/documents/22284544/22295820/ufi_developers_manual_en.pdf/9d47a5c9-ba58-4b5c-8101-7d5610928035

using System;
using System.Collections;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Text.RegularExpressions;


public class UFI
{
	public const string b31chars = "0123456789ACDEFGHJKMNPQRSTUVWXY";
	private static int CalculateChecksum(string ufi)
	{
		if (ufi.Length > 15) ufi = ufi.Substring(1);
		var weightedSum = 0;
		for (int i = 0; i < 15; i++) weightedSum += (i + 2) * b31chars.IndexOf(ufi[i]);
		return (31 - weightedSum % 31) % 31;
	}

	public static bool IsUfiValid(string ufi)
	{
		ufi = Regex.Replace(ufi?.Trim(), "\\-", ""); // remove dashes
		if (ufi.Length != 16) return false; // check length
		if (!ufi.All(c => b31chars.Contains(c))) return false;  // check characters
		var checksum = b31chars.IndexOf(ufi[0]); // checksum (value/index of first character)
		var calculatedChecksum = CalculateChecksum(ufi); // calculate checksum
		return checksum == calculatedChecksum;
	}

	public static string GenerateUFI(string VAT, int formulation)
	{
		if (VAT.Length == 0) return "";

		//2.1.1 Step 1 – UFI payload numerical value
		string countryCodeIso3166 = GetCountryCodeISO3166(VAT);
		long vatLong = VatToNumber(VAT, countryCodeIso3166);
		BitArray group = GetCountryGroup(countryCodeIso3166);
		BitArray country = GetCountryCode(countryCodeIso3166);
		BitArray vatn = VatNumberBits(vatLong, countryCodeIso3166);
		BitArray formulationN = formulation.ToBinary(28);
		BitArray togetherStep1 = 0.ToBinary(1).Append(vatn).Append(country).Append(group).Append(formulationN);

		//2.1.2 Step 2 – UFI payload in base-31
		string decimalPayloadString = BinToDec(togetherStep1.ToDigitString());
		BigInteger payload = BigInteger.Parse(decimalPayloadString);
		string base31PayloadStep2 = BigIntegerToStringBaseX(payload, b31chars).PadLeft(15, '0');

		//2.1.3 Step 3 – Character reorganisation
		string reorganisedStep3 = CharacterReorganisation(base31PayloadStep2);
		//2.1.4 Step 4 – Checksum calculation
		int checkSum = CalculateChecksum(reorganisedStep3);

		// checksum ervoor
		string UFIPlainStep4 = $"{b31chars[checkSum]}{reorganisedStep3}";
		// streepjes ertussen
		return Regex.Replace(UFIPlainStep4, @"^(....)(....)(....)(....)$", "$1-$2-$3-$4");
	}

	public static void ReverseUFI(string ufi)
	{
		if (!IsUfiValid(ufi)) throw new Exception("UFI code is invalid");

		// remove dashes
		ufi = Regex.Replace(ufi?.Trim(), "\\-", ""); 
		// remove first character
		ufi = ufi.Substring(1);
		// backwards character reorganisation
		var reversed = ReverseCharacterReorganisation(ufi);
		// unbase 31
		var bigInt = StringBaseXToBigInteger(reversed, b31chars);
		// convert to base2, pad to 74 characters
		var binary = ToNBase(bigInt, 2).PadLeft(74, '0');
		// split binary parts
		var parts = SplitSizes(binary, 28, 4, 7, 34, 1);
		// convert binary to decimal
		var formulationN = BinaryToDecimal(parts[0]);
		var countryGroup = BinaryToDecimal(parts[1]);
		var country = BinaryToDecimal(parts[2]);
		var vatnr = BinaryToDecimal(parts[3]);

		Console.WriteLine($"{countryGroup}; {country}; {vatnr}; {formulationN}");
	}

	public static string[] SplitSizes(string str, params int[] sizes)
	{
		var length = sizes.Length;
		var parts = new string[length];
		var start = 0;
		for (int i = 0; i < length; i++)
		{
			parts[i] = str.Substring(start, sizes[i]);
			start += sizes[i];
		}
		return parts;
	}

	public static BigInteger BinaryToDecimal(string binary) => StringBaseXToBigInteger(binary, "01");

	public static string ToNBase(BigInteger a, int n)
	{
		var sb = new StringBuilder();
		while (a > 0)
		{
			sb.Insert(0, a % n);
			a /= n;
		}
		return sb.ToString();
	}

	/// <summary>
	/// Table 2-2: Rules for VAT number conversion to numerical value
	/// </summary>
	/// <param name="VAT"></param>
	/// <returns></returns>
	internal static long VatToNumber(string VAT, string Country)
	{
		switch (Country)
		{
			case "CY":
				return VatToNumberCy(VAT);
			case "ES":
				return VatToNumberEs(VAT);
			case "FR":
				return VatToNumberFr(VAT);
			case "GB":
				return VatToNumberGb(VAT);
			case "IE":
				return VatToNumberIe(VAT);
			case "IS":
				return VatToNumberIs(VAT);
			default:
				string justNumbers = new string(VAT.Where(char.IsDigit).ToArray());
				long.TryParse(justNumbers, out long n);
				return n;
		}
	}

	private static long VatToNumberCy(string VAT)
	{
		string decimalPart = VAT.Substring(2, 8);
		char letter = VAT.Substring(10, 1)[0];
		int letterIndex = LetterNumberCyGb(letter);

		long letterPart = letterIndex * (long)Math.Pow(10, 8);
		return letterPart + Convert.ToInt64(decimalPart);
	}

	internal static long VatToNumberEs(string VAT)
	{
		string decimalPart = VAT.Substring(3, 7);
		int c1 = LetterNumberEsFrIs(VAT.Substring(2, 1)[0]);
		int c2 = LetterNumberEsFrIs(VAT.Substring(10, 1)[0]);

		long letterPart = (36 * c1 + c2) * (long)Math.Pow(10, 7);
		return letterPart + Convert.ToInt64(decimalPart);
	}

	internal static long VatToNumberFr(string VAT)
	{
		//FRAB123456789
		string decimalPart = VAT.Substring(4, 9);
		int c1 = LetterNumberEsFrIs(VAT.Substring(2, 1)[0]);
		int c2 = LetterNumberEsFrIs(VAT.Substring(3, 1)[0]);

		long letterPart = (36 * c1 + c2) * (long)Math.Pow(10, 9);
		return letterPart + Convert.ToInt64(decimalPart);
	}

	internal static long VatToNumberGb(string VAT)
	{
		//GB123456789012 or GBAB123
		string justNumbers = new string(VAT.Where(char.IsDigit).ToArray());
		long.TryParse(justNumbers, out long n);

		if (VAT.Length == 7)
		{
			int c1 = LetterNumberCyGb(VAT.Substring(2, 1)[0]);
			int c2 = LetterNumberCyGb(VAT.Substring(3, 1)[0]);
			long letterPart = (26 * c1 + c2) * (long)Math.Pow(10, 3);
			return letterPart + n;
		}
		else
		{
			return (long)Math.Pow(2, 40) + n;
		}
	}

	internal static long VatToNumberIe(string VAT)
	{
		Regex regexA = new Regex("[0-9][A-Z*+][0-9]{5}[A-Z]");
		Match matchA = regexA.Match(VAT);

		if (matchA.Success)
		{
			//"IE9*54321Y"
			string d = VAT.Substring(2, 1) + VAT.Substring(VAT.Length - 6, 5);
			int c1 = LetterNumberIe(VAT.Substring(3, 1)[0]);
			int c2 = LetterNumberIe(VAT.Substring(VAT.Length - 1, 1)[0]);

			long letterPart = (26 * c1 + c2) * (long)Math.Pow(10, 6);
			return letterPart + Convert.ToInt64(d);
		}
		else
		{
			//IE9876543Z
			string justNumbers = new string(VAT.Where(char.IsDigit).ToArray());
			long.TryParse(justNumbers, out long N);
			int c1 = LetterNumberCyGb(VAT.Substring(9, 1)[0]);
			int c2 = 0;
			if (VAT.Length == 11)
				c2 = LetterNumberCyGb(VAT.Substring(10, 1)[0]);

			return ((long)Math.Pow(2, 33) + ((26 * c2 + c1) * (long)Math.Pow(10, 7) + Convert.ToInt64(N)));
		}
	}

	internal static long VatToNumberIs(string VAT)
	{
		//ISAB3D5F
		long VN = 0;
		char l1 = VAT.Substring(7, 1)[0];
		char l2 = VAT.Substring(6, 1)[0];
		char l3 = VAT.Substring(5, 1)[0];
		char l4 = VAT.Substring(4, 1)[0];
		char l5 = VAT.Substring(3, 1)[0];
		char l6 = VAT.Substring(2, 1)[0];

		VN += 36 * 36 * 36 * 36 * 36 * LetterNumberEsFrIs(l6);
		VN += 36 * 36 * 36 * 36 * LetterNumberEsFrIs(l5);
		VN += 36 * 36 *36  * LetterNumberEsFrIs(l4);
		VN += 36 * 36 * LetterNumberEsFrIs(l3);
		VN += 36 * LetterNumberEsFrIs(l2);
		VN += LetterNumberEsFrIs(l1);
		return VN;
	}

	private static int LetterNumberIe(char c)
	{
		if (c == '+') return 26;
		if (c == '*') return 27;
		return char.ToUpper(c) - 65;
	}

	private static int LetterNumberCyGb(char c)
	{
		return char.ToUpper(c) - 65;
	}

	internal static int LetterNumberEsFrIs(char c)
	{
		if (char.IsDigit(c)) return (int)char.GetNumericValue(c);
		return char.ToUpper(c) - 55;
	}

	static BitArray VatNumberBits(long VAT, string countryCode)
	{
		return VAT.ToBinary(41 - GetBitsCount(countryCode));
	}

	private static readonly int[] mapping = new int[] { 5, 4, 3, 7, 2, 8, 9, 10, 1, 0, 11, 6, 12, 13, 14 };
	private static readonly int[] reverseMapping = new int[] { 9, 8, 4, 2, 1, 0, 11, 3, 5, 6, 7, 10, 12, 13, 14 };

	private static string CharacterReorganisation(string b31)
	{
		var sb = new StringBuilder();
		for (int i = 0; i < 15; i++)
			sb.Append(b31[mapping[i]]);
		return sb.ToString();
	}

	private static string ReverseCharacterReorganisation(string b31)
	{
		var sb = new StringBuilder();
		for (int i = 0; i < 15; i++)
			sb.Append(b31[reverseMapping[i]]);
		return sb.ToString();
	}

	/// <summary>
	/// Binary string to decimal string
	/// 0101 => 5
	/// </summary>
	private static string BinToDec(string value)
	{
		BigInteger res = 0;
		foreach (char c in value)
		{
			res <<= 1;
			res += c == '1' ? 1 : 0;
		}
		return res.ToString();
	}

	/// <summary>
	/// BigInteger to any base string
	/// </summary>
	private static string BigIntegerToStringBaseX(BigInteger value, string baseChars)
	{
		string result = string.Empty;
		int targetBase = baseChars.Length;
		do
		{
			result = baseChars[(int)(value % targetBase)] + result;
			value /= targetBase;
		}
		while (value > 0);
		return result;
	}

	private static BigInteger StringBaseXToBigInteger(string ufi, string baseChars)
	{
		BigInteger value = 0;
		var currentBase = baseChars.Length;
		var reversed = ufi.Reverse().ToList();
		for (int i = 0; i < reversed.Count; i++)
		{
			var baseValue = baseChars.IndexOf(reversed[i]);
			var digitValue = BigInteger.Pow(new BigInteger(currentBase), i);
			value += digitValue * baseValue;
		}
		return value;
	}

	private static BitArray GetCountryGroup(string countryCode)
	{
		switch (countryCode)
		{
			case "":
				return 0.ToBinary(4);
			case "FR":
				return 1.ToBinary(4);
			case "GB":
				return 2.ToBinary(4);
			case "LT":
			case "SE":
				return 3.ToBinary(4);
			case "HR":
			case "IT":
			case "LV":
			case "NL":
				return 4.ToBinary(4);
			default:
				return 5.ToBinary(4);
		}
	}

	private static int GetBitsCount(string countryCode)
	{
		switch (countryCode)
		{
			case "":
			case "FR":
			case "GB":
				return 0;
			case "LT":
			case "SE":
				return 1;
			case "HR":
			case "IT":
			case "LV":
			case "NL":
				return 4;
			default:
				return 7;
		}
	}

	private static BitArray GetCountryCode(string countryCode)
	{
		switch (countryCode)
		{
			default:
			case "":
			case "FR":
			case "GB":
				return new BitArray(0);
			case "LT":
				return 0.ToBinary(1);
			case "SE":
				return 1.ToBinary(1);
			case "HR":
				return 0.ToBinary(4);
			case "IT":
				return 1.ToBinary(4);
			case "LV":
				return 2.ToBinary(4);
			case "NL":
				return 3.ToBinary(4);
			case "BG":
				return 0.ToBinary(7);
			case "CZ":
				return 1.ToBinary(7);
			case "IE":
				return 2.ToBinary(7);
			case "ES":
				return 3.ToBinary(7);
			case "PL":
				return 4.ToBinary(7);
			case "RO":
				return 5.ToBinary(7);
			case "SK":
				return 6.ToBinary(7);
			case "CY":
				return 7.ToBinary(7);
			case "IS":
				return 8.ToBinary(7);
			case "BE":
				return 9.ToBinary(7);
			case "DE":
				return 10.ToBinary(7);
			case "EE":
				return 11.ToBinary(7);
			case "GR":
				return 12.ToBinary(7);
			case "NO":
				return 13.ToBinary(7);
			case "PT":
				return 14.ToBinary(7);
			case "AT":
				return 15.ToBinary(7);
			case "DK":
				return 16.ToBinary(7);
			case "FI":
				return 17.ToBinary(7);
			case "HU":
				return 18.ToBinary(7);
			case "LU":
				return 19.ToBinary(7);
			case "MT":
				return 20.ToBinary(7);
			case "SI":
				return 21.ToBinary(7);
			case "LI":
				return 22.ToBinary(7);
		}
	}

	/// <summary>
	/// Uppercase ISO-3166-1 alpha-2 country codes from VAT
	/// </summary>
	/// <param name="VAT"></param>
	/// <returns></returns>
	private static string GetCountryCodeISO3166(string VAT)
	{
		if (VAT.Length == 0) return "";
		if (char.IsDigit(VAT[0])) return "";

		string code = VAT.Substring(0, 2).ToUpper();
		if (code == "EL") code = "GR";
		return code;
	}
}

public static class Extensions
{
	public static string ToDigitString(this BitArray array)
	{
		StringBuilder builder = new StringBuilder();
		foreach (bool bit in array.Cast<bool>())
			builder.Append(bit ? "1" : "0");

		return Reverse(builder.ToString());
	}

	private static string Reverse(string s)
	{
		char[] charArray = s.ToCharArray();
		Array.Reverse(charArray);
		return new string(charArray);
	}

	public static BitArray Append(this BitArray current, BitArray after)
	{
		bool[] bools = new bool[current.Count + after.Count];
		current.CopyTo(bools, 0);
		after.CopyTo(bools, current.Count);
		return new BitArray(bools);
	}

	public static BitArray ToBinary(this int numeral, int bitCount)
	{
		BitArray binary = new BitArray(new[] { numeral }) { Length = bitCount };
		return binary;
	}

	public static BitArray ToBinary(this long numeral, int bitCount)
	{
		BitArray binary = new BitArray(BitConverter.GetBytes(numeral)) { Length = bitCount };
		return binary;
	}
}
40820cookie-checkUFI (Unique Formula Identifier)