четверг, 6 марта 2014 г.

Формирование и проверка УИН ГИБДД

С февраля 2014 в России вводится новый Универсальный Идентификатор начислений (УИН) http://savepayment.ru/soft/53/uin-v-platezhnom-poruchenii. Это 20-ти значная строка, которая содержит цифры и русские и латинские буквы. И если ранее штраф идентифицировался по номеру и дате платежного постановления, то теперь будут УИНы. В принципе, можно осуществить конвертацию УИН в номер дату платежного постановления и обратно. Алгоритм формирования УИН для ГИБДД описан тут (осторожно, множество опечаток в тестовых данных): Письмо МВД России от 9 августа 2013 г. № 13/9-4902 "Об информировании кредитных организаций о правилах формирования уникального идентификатора начислений" 
UPDATE: с какого-то момента 2014 года опять все поменялось, и УИН ГИБДД теперь содержит только цифры, без букв, и в нем уже не закодирован номер и дата постановления. http://savepayment.ru/recommendation/51/kvitantsiya-gibdd-izmenilas  из такого УИНа нельзя достать номер и дату постановления!
Непосредственно реализация конвертации УИН старого образца в пару номер/дата постановления

public class UIN {
    private static final char[] map =   {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
            'А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ж', 'З', 'И', 'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Ь', 'Э', 'Ю', 'Я'};


    private static final int[] weights = { 0,  1,   2,   3,   4,   5,   6,   7,   8,   9,
            1,   3,   17,  29,   6,   30,  31, 13,  32,  33,  10,  34,  12,  35,  14,  16,  36,  37,  38,  18,  39,  40,  41,  21,  19,  42,
            1,   2,   3,   4,    5,   6,   7,  8,   9,   10,  11,  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  42,  26,  27,  28 };


    public static boolean checkCRC(String uin){
        if(uin.length() != 20){
            throw new RuntimeException("incorrect uin length");
        }
        String finalNumber = uin.substring(0, 19);
        int crc = calculateCRC(finalNumber, 1);
        if(crc % 11 == 10){
            crc = calculateCRC(finalNumber, 3);
            if(crc %11 == 10){
                crc = 0;
            }
        }
        crc = crc%11;
        return uin.charAt(19) == Character.forDigit(crc, 10);
    }

    public static String getFineNumber(String uin){
        if(uin.length() != 20){
            throw new RuntimeException("incorrect uin length");
        }
        return uin.substring(7,19).replaceAll("(Z){1,12}$", "");
    }


    public static Date getFineDate(String uin){
        if(uin.length() != 20){
            throw new RuntimeException("incorrect uin length");
        }
        int res = 0;
        String dateStr = uin.substring(5,7);
        for (int i = dateStr.length(); i > 0; i--) {
            int index = lookupIndex(dateStr.charAt(dateStr.length() - i));
            res += index * Math.pow(64, i - 1);
        }
        Calendar c = Calendar.getInstance();
        int year = c.get(Calendar.YEAR);
        if (year % 10 == 0 && res % 10 != 0) { // в 2020 году могут оплатить штраф за 2019-й
            c.set(Calendar.YEAR, ((year / 10) - 1) * 10 + res % 10);
        } else {
            c.set(Calendar.YEAR, (year / 10) * 10 + res % 10);
        }
        c.set(Calendar.DAY_OF_YEAR, res / 10); // важно сначала установить год, а потом день года
        return c.getTime();
    }

    private static int lookupIndex(char ch) {
        for (int i = 0; i < map.length; i++) {
            if (map[i] == ch) {
                return i;
            }
        }
        throw new RuntimeException("Invalid character: '" + ch + "'");
    }

    private static int calculateCRC(String finalNumber, int startIndex) {
        int crc = 0;
        int i = startIndex;
        for(char ch : finalNumber.toCharArray()){
            crc += i++ * (weights[lookupIndex(ch)]%10);
            if(i==11){
                i=1;
            }
        }
        return crc;
    }
}

юнит-тест, использующий исправленные входные данные отсюда: http://www.garant.ru/products/ipo/prime/doc/70339846

@Test
    public void testUIN() throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");

        Assert.assertTrue(UIN.checkCRC("18810R564BK229683ZZ8"));
        Assert.assertTrue(UIN.checkCRC("18810ЗД57KT002522ZZ2"));
        Assert.assertTrue(UIN.checkCRC("18810HФ50АО309188ZZ5"));

        Assert.assertEquals("64BK229683", UIN.getFineNumber("18810R564BK229683ZZ8"));
        Assert.assertEquals("57KT002522", UIN.getFineNumber("18810ЗД57KT002522ZZ2"));
        Assert.assertEquals("50АО309188", UIN.getFineNumber("18810HФ50АО309188ZZ5"));

        Assert.assertEquals("22.06.2013", sdf.format(UIN.getFineDate("18810R564BK229683ZZ8")));
        Assert.assertEquals("24.04.2013", sdf.format(UIN.getFineDate("18810HФ50АО309188ZZ5")));
        Assert.assertEquals("05.10.2012", sdf.format(UIN.getFineDate("18810ЗД57KT002522ZZ2")));
    }


и небольшой юнит-тест, который использует те-же функции, для обратной конвертации. Он не оформлен как класс и написан непонятно зачем:
    @Test
    public void testPackUinDate() throws Exception {
        String number ="64BK229683";
        String dateStr = "22.06.2013";
        Date d = new SimpleDateFormat("dd.MM.yyyy").parse(dateStr);

        Calendar c = Calendar.getInstance();
        c.setTime(d);

        int tmp = (c.get(Calendar.DAY_OF_YEAR)) * 10 + (c.get(Calendar.YEAR) % 10);
        String result = "";
        while (tmp > 64) {
            result += map[tmp % 64];
            tmp = tmp / 64;
        }
        result += map[tmp];
        Assert.assertEquals("R5", StringUtils.reverse(result));
        String finalNumber = "18810" +  StringUtils.reverse(result) +  StringUtils.rightPad(number, 12, 'Z');
        Assert.assertEquals("18810R564BK229683ZZ", finalNumber);

        int crc = calculateCRC(finalNumber, 1);
        if(crc % 11 == 10){
            crc = calculateCRC(finalNumber, 3);
            if(crc %11 == 10){
                crc = 0;
            }
        }
        crc = crc%11;
        Assert.assertEquals(8, crc);
        Assert.assertEquals("18810R564BK229683ZZ8", finalNumber+crc);

    }