TBCDCharset.java

/*
 * Copyright (c) 2020, Stein Eldar Johnsen
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package net.morimekta.strings.enc;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;

/**
 * Telephony Binary Coded Decimal String. This is a charset that encodes
 * numbers for telephony packing 2 digits per byte. The encoding is very
 * restrictive, but does not insert 'unknown' characters where there is
 * bad data or chars.
 *
 * <pre>
 *     [0123456789#*abc]
 * </pre>
 *
 * The 'odd' variety is one that when decoding will ignore the last 'even'
 * number (as the numbers come in pairs), making the number of characters
 * decoded always odd. It will also encode the last character as '0', but
 * will accept and encode an even number of characters, though will then
 * not decode as the same string.
 */
public class TBCDCharset extends Charset {
    /** TBDC default charset. */
    public static final TBCDCharset TBCD = new TBCDCharset(false);
    /** TBDC odd digits charset. */
    public static final TBCDCharset TBCD_ODD = new TBCDCharset(true);

    private TBCDCharset(boolean odd) {
        super("TBCD" + (odd ? "-odd" : ""),
              new String[]{"BCD" + (odd ? "-odd" : "")});
        this.odd = odd;
    }

    @Override
    public boolean contains(Charset charset) {
        return charset instanceof TBCDCharset;
    }

    @Override
    public CharsetDecoder newDecoder() {
        return new Decoder(this);
    }

    @Override
    public CharsetEncoder newEncoder() {
        return new Encoder(this);
    }

    static class Encoder extends CharsetEncoder {
        private final boolean odd;
        private int lastEncodedDigit = -1;

        protected Encoder(TBCDCharset charset) {
            super(charset, 0.55f, 1, new byte[]{(byte) 0x1a});
            this.odd = charset.odd;
        }

        @Override
        public boolean canEncode(char c) {
            return forDigit(c) != 15;
        }

        @Override
        public boolean canEncode(CharSequence cs) {
            return cs.chars().allMatch(c -> canEncode((char) c));
        }

        @Override
        protected CoderResult encodeLoop(CharBuffer charBuffer, ByteBuffer byteBuffer) {
            while (lastEncodedDigit >= 0 || charBuffer.hasRemaining()) {
                int d1 = lastEncodedDigit >= 0 ? lastEncodedDigit : forDigit(charBuffer.get());
                lastEncodedDigit = -1;
                int d2 = odd ? 0 : 15;
                if (charBuffer.hasRemaining()) {
                    d2 = forDigit(charBuffer.get());
                }
                byteBuffer.put(i2b(d2 << 4 | d1));
            }
            return CoderResult.UNDERFLOW;
        }
    }

    static class Decoder extends CharsetDecoder {
        private final boolean odd;

        protected Decoder(TBCDCharset charset) {
            super(charset, 2.f, 2.f);
            this.odd = charset.odd;
        }

        @Override
        protected CoderResult decodeLoop(ByteBuffer byteBuffer, CharBuffer charBuffer) {
            while (byteBuffer.hasRemaining()) {
                int bt = b2i(byteBuffer.get());
                char d1 = toDigit(bt & 0x0f);
                charBuffer.put(d1);
                char d2 = toDigit(bt >> 4);
                if (byteBuffer.hasRemaining() || (!odd && d2 != '�')) {
                    // Only skip if very last digit is unknown.
                    charBuffer.put(d2);
                }
            }
            return CoderResult.UNDERFLOW;
        }
    }

    private final boolean odd;

    private static byte i2b(int i) {
        return (byte) i;
    }

    private static int b2i(byte b) {
        if (b < 0) return 0x100 + b;
        return b;
    }

    private static char toDigit(int b) {
        switch (b) {
            case 0:
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
            case 6:
            case 7:
            case 8:
            case 9:
                return (char) ('0' + b);
            case 10: return '*';
            case 11: return '#';
            case 12: return 'a';
            case 13: return 'b';
            case 14: return 'c';
            default: return '�';
        }
    }

    private static int forDigit(char d) {
        switch (d) {
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                return (byte) (d - '0');
            case '*':
                return 10;
            case '#':
                return 11;
            case 'a':
            case 'A':
                return 12;
            case 'b':
            case 'B':
                return 13;
            case 'c':
            case 'C':
                return 14;
            default:
                return 15;
        }
    }
}