UNPKG

@citrineos/util

Version:

The OCPP util module which supplies helpful utilities like cache and queue connectors, etc.

179 lines 6.65 kB
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project // // SPDX-License-Identifier: Apache-2.0 /** * Calculate check digit for eMAID according to eMI³ specification * Based on the algorithm described in "Check Digit Calculation for Contract-IDs" * * This implementation can detect five most frequent error types: * 1. Single error: one character is wrong * 2. Adjacent transposition: two adjacent characters are swapped * 3. Twin error: two identical adjacent characters are both changed * 4. Jump transposition: abc becomes cba * 5. Jump twin error: aca becomes bcb * * @param emaidWithoutCheckDigit - The first 14 characters of the eMAID (without check digit) * @returns The calculated check digit character */ export function calculateCheckDigit(emaidWithoutCheckDigit) { // Ensure input is uppercase and 14 characters const input = emaidWithoutCheckDigit.toUpperCase(); if (input.length !== 14) { throw new Error('Input must be exactly 14 characters'); } // Validate that input contains only alphanumeric characters if (!/^[A-Z0-9]+$/.test(input)) { throw new Error('Input must contain only alphanumeric characters'); } // Lookup tables for q1, q2, r1, r2 values based on the PDF specification const lookupTable = { '0': { q1: 0, q2: 0, r1: 0, r2: 0 }, '1': { q1: 0, q2: 0, r1: 0, r2: 1 }, '2': { q1: 0, q2: 0, r1: 0, r2: 2 }, '3': { q1: 0, q2: 0, r1: 1, r2: 0 }, '4': { q1: 0, q2: 0, r1: 1, r2: 1 }, '5': { q1: 0, q2: 0, r1: 1, r2: 2 }, '6': { q1: 0, q2: 0, r1: 2, r2: 0 }, '7': { q1: 0, q2: 0, r1: 2, r2: 1 }, '8': { q1: 0, q2: 0, r1: 2, r2: 2 }, '9': { q1: 0, q2: 1, r1: 0, r2: 0 }, A: { q1: 0, q2: 1, r1: 0, r2: 1 }, B: { q1: 0, q2: 1, r1: 0, r2: 2 }, C: { q1: 0, q2: 1, r1: 1, r2: 0 }, D: { q1: 0, q2: 1, r1: 1, r2: 1 }, E: { q1: 0, q2: 1, r1: 1, r2: 2 }, F: { q1: 0, q2: 1, r1: 2, r2: 0 }, G: { q1: 0, q2: 1, r1: 2, r2: 1 }, H: { q1: 0, q2: 1, r1: 2, r2: 2 }, I: { q1: 1, q2: 0, r1: 0, r2: 0 }, J: { q1: 1, q2: 0, r1: 0, r2: 1 }, K: { q1: 1, q2: 0, r1: 0, r2: 2 }, L: { q1: 1, q2: 0, r1: 1, r2: 0 }, M: { q1: 1, q2: 0, r1: 1, r2: 1 }, N: { q1: 1, q2: 0, r1: 1, r2: 2 }, O: { q1: 1, q2: 0, r1: 2, r2: 0 }, P: { q1: 1, q2: 0, r1: 2, r2: 1 }, Q: { q1: 1, q2: 0, r1: 2, r2: 2 }, R: { q1: 1, q2: 1, r1: 0, r2: 0 }, S: { q1: 1, q2: 1, r1: 0, r2: 1 }, T: { q1: 1, q2: 1, r1: 0, r2: 2 }, U: { q1: 1, q2: 1, r1: 1, r2: 0 }, V: { q1: 1, q2: 1, r1: 1, r2: 1 }, W: { q1: 1, q2: 1, r1: 1, r2: 2 }, X: { q1: 1, q2: 1, r1: 2, r2: 0 }, Y: { q1: 1, q2: 1, r1: 2, r2: 1 }, Z: { q1: 1, q2: 1, r1: 2, r2: 2 }, }; // Matrix P1 powers over Z2 (modulo 2) // The pattern repeats every 3 positions const getP1Power = (index) => { const position = (index % 3) + 1; if (position === 3) { return [ [1, 0], [0, 1], ]; // Identity matrix for positions 3, 6, 9, 12, 15 } else if (position === 1) { return [ [0, 1], [1, 1], ]; // P1 for positions 1, 4, 7, 10, 13 } else { return [ [1, 1], [1, 0], ]; // P1^2 for positions 2, 5, 8, 11, 14 } }; // Matrix P2 powers over Z3 (modulo 3) // The pattern repeats every 8 positions const P2_powers = [ [ [0, 1], [1, 2], ], // P2^1 [ [1, 2], [2, 2], ], // P2^2 [ [2, 2], [2, 0], ], // P2^3 [ [2, 0], [0, 2], ], // P2^4 [ [0, 2], [2, 1], ], // P2^5 [ [2, 1], [1, 1], ], // P2^6 [ [1, 1], [1, 0], ], // P2^7 [ [1, 0], [0, 1], ], // P2^8 (Identity) ]; const getP2Power = (index) => { return P2_powers[index % 8]; }; // Initialize accumulators for check equations const q_sum = [0, 0]; // For Z2 calculation const r_sum = [0, 0]; // For Z3 calculation // Process each character for (let i = 0; i < 14; i++) { const char = input[i]; const values = lookupTable[char]; if (!values) { throw new Error(`Invalid character '${char}' at position ${i}`); } // Matrix multiplication for q (Z2) const P1 = getP1Power(i); const q_vec = [values.q1, values.q2]; const q_result = [ (P1[0][0] * q_vec[0] + P1[0][1] * q_vec[1]) % 2, (P1[1][0] * q_vec[0] + P1[1][1] * q_vec[1]) % 2, ]; q_sum[0] = (q_sum[0] + q_result[0]) % 2; q_sum[1] = (q_sum[1] + q_result[1]) % 2; // Matrix multiplication for r (Z3) const P2 = getP2Power(i); const r_vec = [values.r1, values.r2]; const r_result = [ (P2[0][0] * r_vec[0] + P2[0][1] * r_vec[1]) % 3, (P2[1][0] * r_vec[0] + P2[1][1] * r_vec[1]) % 3, ]; r_sum[0] = (r_sum[0] + r_result[0]) % 3; r_sum[1] = (r_sum[1] + r_result[1]) % 3; } // Calculate check digit values // For position 15: P1^15 = Identity, P2^15 = [[1,1],[1,0]] // Solve for q15 such that the check equation equals 0 // Since P1^15 is identity, q15 = -q_sum (mod 2) const q15 = [(2 - q_sum[0]) % 2, (2 - q_sum[1]) % 2]; // Solve for r15 such that the check equation equals 0 // For P2^15 = [[1,1],[1,0]], we solve: [r15_1, r15_2] * P2^15 = -r_sum // This gives: r15_1 = -r_sum[1] and r15_1 + r15_2 = -r_sum[0] const r15 = [(3 - r_sum[1]) % 3, (((3 - r_sum[0]) % 3) + r_sum[1]) % 3]; // Convert from two-dimensional to single values const q_value = q15[0] * 2 + q15[1]; // Convert from Z2 × Z2 to {0,1,2,3} const r_value = r15[0] * 3 + r15[1]; // Convert from Z3 × Z3 to {0,1,2,...,8} // Combine using the formula: a = q * 9 + r const checkValue = q_value * 9 + r_value; // Map the check value (0-35) directly to alphanumeric characters const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (checkValue < 0 || checkValue >= 36) { throw new Error(`Invalid check value: ${checkValue}`); } return charset[checkValue]; } //# sourceMappingURL=emaidCheckDigitCalculator.js.map