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;
}
}
}