com.offbynull.voip.kademlia.model.BitString.java Source code

Java tutorial

Introduction

Here is the source code for com.offbynull.voip.kademlia.model.BitString.java

Source

/*
 * Copyright (c) 2015, Kasra Faghihi, All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.offbynull.voip.kademlia.model;

import java.io.Serializable;
import java.util.Arrays;
import org.apache.commons.lang3.Validate;

/**
 * Bit string.
 * <p>
 * Class is immutable.
 * @author Kasra Faghihi
 */
public final class BitString implements Serializable {
    private static final long serialVersionUID = 1L;

    private final byte[] data; // treated as an array of bit starting from bit 0
    private final int bitLength;

    // make sure that whatever you pass in as data is a copy / not-shared.
    private BitString(byte[] data, int bitLength) {
        Validate.notNull(data);
        Validate.isTrue(bitLength >= 0);

        int minLength = calculateRequiredByteArraySize(bitLength);
        Validate.isTrue(data.length == minLength);

        this.data = data;
        this.bitLength = bitLength;
    }

    /**
     * Constructs a {@link BitString} from a string. Bits from string are read in read-order (starting from position 0 onward). Character
     * {@code '0'} is represented as bit {@code 0}, and character {@code '1'} is represented as bit {@code 1}.
     * @param data string to read bitstring data from
     * @return created bitstring
     * @throws IllegalArgumentException if any chracter other than {@code '0'} or {@code '1'} is encountered in {@code data}
     * @throws NullPointerException if any argument is {@code null}
     */
    public static BitString createFromString(String data) {
        Validate.notNull(data);

        int offset = 0;
        int len = data.length();

        int arrLen = calculateRequiredByteArraySize(len);
        int arrIdx = 0;
        byte[] arr = new byte[arrLen];

        int end = offset + len;
        int currOffset = offset;
        byte[] temp = new byte[1];
        while (currOffset < end) {
            int nextOffset = Math.min(currOffset + 8, end);
            int currLen = nextOffset - currOffset;

            try {
                temp[0] = (byte) (Integer.valueOf(data.substring(currOffset, currOffset + currLen), 2) & 0xFF);
            } catch (NumberFormatException nfe) {
                throw new IllegalArgumentException(nfe);
            }

            if (currLen < 8) {
                temp[0] = (byte) ((temp[0] << (8 - currLen)) & 0xFF);
            }

            byte b = readBitsFromByteArrayInReadOrder(temp, 0, currLen);
            arr[arrIdx] = b;

            arrIdx++;
            currOffset = nextOffset;
        }

        return new BitString(arr, len);
    }

    /**
     * Constructs a {@link BitString} from a byte array. Bits from input array are read in logical-order (starting from bit 0 onward). This
     * seems counter-intuitive because an array of bytes is represented from left-to-right (byte 0 is leftmost) while an array of bits is
     * represented from right-to-left (bit 0 is the right-most). So for example, the input array {@code [0x04, 0xFB]} with offset of 2 and
     * length of 10 would result in bitstring {@code 1000 0011 01}.
     * <p>
     * Read-order representation {@code [0x01, 0xFB]}, where bits are ordered from right-to-left...
     * <pre>
     * Byte    0                 1
     * Bit     7 6 5 4 3 2 1 0   7 6 5 4 3 2 1 0
     *         ---------------------------------
     *         0 0 0 0 0 1 0 0   1 1 1 1 1 0 1 1
     *         0       4         F       B
     * </pre>
     * Logical-order representation {@code [0x01, 0xFB]}, where bits are ordered from left-to-right ...
     * <pre>
     * Byte    0                 1
     * Bit     0 1 2 3 4 5 6 7   0 1 2 3 4 5 6 7
     *         ---------------------------------
     *         0 0 1 0 0 0 0 0   1 1 0 1 1 1 1 1
     *               4       0         B       F
     *             ^                   ^
     *             |                   |
     *          offset (2)        offset+len (2+10)
     * </pre>
     * The logical-order representation is the way bits are read in.
     * @param data array to read bitstring data from
     * @param offset bit position to read from
     * @param len number of bits to read
     * @return created bitstring
     * @throws NullPointerException if any argument is {@code null}
     * @throws IllegalArgumentException if {@code bitLength <= 0}, or if {@code data} is larger than the minimum number of bytes that it
     * takes to retain {@code bitLength} (e.g. if you're retaining 12 bits, you need 2 bytes or less -- {@code 12/8 + (12%8 == 0 ? 0 : 1)})
     */
    public static BitString createLogicalOrder(byte[] data, int offset, int len) {
        Validate.notNull(data);
        Validate.isTrue(offset >= 0);
        Validate.isTrue(len >= 0);
        Validate.isTrue(offset + len <= data.length * 8);

        int arrLen = calculateRequiredByteArraySize(len);
        int arrIdx = 0;
        byte[] arr = new byte[arrLen];

        int end = offset + len;
        int currOffset = offset;
        while (currOffset < end) {
            int nextOffset = Math.min(currOffset + 8, end);
            int currLen = nextOffset - currOffset;

            byte b = readBitsFromByteArrayInLogicalOrder(data, currOffset, currLen);
            arr[arrIdx] = b;

            arrIdx++;
            currOffset = nextOffset;
        }

        return new BitString(arr, len);
    }

    /**
     * Constructs a {@link BitString} from a byte array. Bits from input array are read in read-order (the order you would read a bytes in
     * a byte array). So for example, the input array {@code [0x04, 0xFB]} with offset of 2 and length of 10 would result in bitstring
     * {@code 0001 0011 11}.
     * <p>
     * Read-order representation {@code [0x01, 0xFB]}, where bits are ordered from right-to-left...
     * <pre>
     * Byte    0                 1
     * Bit     7 6 5 4 3 2 1 0   7 6 5 4 3 2 1 0
     *         ---------------------------------
     *         0 0 0 0 0 1 0 0   1 1 1 1 1 0 1 1
     *         0       4         F       B
     *             ^                   ^
     *             |                   |
     *          offset (2)        offset+len (2+10)
     * </pre>
     * Logical-order representation {@code [0x01, 0xFB]}, where bits are ordered from left-to-right ...
     * <pre>
     * Byte    0                 1
     * Bit     0 1 2 3 4 5 6 7   0 1 2 3 4 5 6 7
     *         ---------------------------------
     *         0 0 1 0 0 0 0 0   1 1 0 1 1 1 1 1
     *               4       0         B       F
     * </pre>
     * The read-order representation is the way bits are read in.
     * @param data array to read bitstring data from
     * @param offset bit position to read from
     * @param len number of bits to read
     * @return created bitstring
     * @throws NullPointerException if any argument is {@code null}
     * @throws IllegalArgumentException if {@code bitLength <= 0}, or if {@code data} is larger than the minimum number of bytes that it
     * takes to retain {@code bitLength} (e.g. if you're retaining 12 bits, you need 2 bytes or less -- {@code 12/8 + (12%8 == 0 ? 0 : 1)})
     */
    public static BitString createReadOrder(byte[] data, int offset, int len) {
        Validate.notNull(data);
        Validate.isTrue(offset >= 0);
        Validate.isTrue(len >= 0);
        Validate.isTrue(offset + len <= data.length * 8);

        int arrLen = calculateRequiredByteArraySize(len);
        int arrIdx = 0;
        byte[] arr = new byte[arrLen];

        int end = offset + len;
        int currOffset = offset;
        while (currOffset < end) {
            int nextOffset = Math.min(currOffset + 8, end);
            int currLen = nextOffset - currOffset;

            byte b = readBitsFromByteArrayInReadOrder(data, currOffset, currLen);
            arr[arrIdx] = b;

            arrIdx++;
            currOffset = nextOffset;
        }

        return new BitString(arr, len);
    }

    // Why work on longs? It may be more efficient to have the byte[] containing the bitstring be a long[].
    private static byte readBitsFromByteArrayInLogicalOrder(byte[] container, int offset, int len) {
        Validate.isTrue(len <= 8);
        Validate.isTrue(offset + len <= container.length * 8);

        int byteOffset = offset / 8;
        int bitOffset = offset % 8;

        int idx = byteOffset;

        int lenOfBitsRemainingInByte = 8 - bitOffset;

        byte ret;
        if (len <= lenOfBitsRemainingInByte) {
            ret = (byte) (isolateBitsToBottom(container[idx], bitOffset, len) & 0xFFL);
        } else {
            long byte1 = container[idx] & 0xFFL;
            long byte2 = container[idx + 1] & 0xFFL;
            int byte1BitOffset = bitOffset;
            int byte1BitLen = Math.min(8 - bitOffset, len);
            int byte2BitOffset = 0;
            int byte2BitLen = len - byte1BitLen;

            long portion1 = isolateBitsToBottom(byte1, byte1BitOffset, byte1BitLen);
            long portion2 = isolateBitsToBottom(byte2, byte2BitOffset, byte2BitLen);

            long combined = (portion1 << byte2BitLen) | portion2;

            ret = (byte) (combined & 0xFF);
        }

        return ret;
    }

    // Why work on longs? It may be more efficient to have the byte[] containing the bitstring be a long[].
    private static byte readBitsFromByteArrayInReadOrder(byte[] container, int offset, int len) {
        Validate.isTrue(len <= 8);
        Validate.isTrue(offset + len <= container.length * 8);

        int byteOffset = offset / 8;
        int bitOffset = offset % 8;

        int idx = byteOffset;

        int lenOfBitsRemainingInByte = 8 - bitOffset;

        byte ret;
        if (len <= lenOfBitsRemainingInByte) {
            long byte1 = Long.reverse(container[idx] & 0xFFL) >>> 56;
            ret = (byte) (isolateBitsToBottom(byte1, bitOffset, len) & 0xFFL);
        } else {
            long byte1 = Long.reverse(container[idx] & 0xFFL) >>> 56;
            long byte2 = Long.reverse(container[idx + 1] & 0xFFL) >>> 56;
            int byte1BitOffset = bitOffset;
            int byte1BitLen = Math.min(8 - bitOffset, len);
            int byte2BitOffset = 0;
            int byte2BitLen = len - byte1BitLen;

            long portion1 = isolateBitsToBottom(byte1, byte1BitOffset, byte1BitLen);
            long portion2 = isolateBitsToBottom(byte2, byte2BitOffset, byte2BitLen);

            long combined = (portion1 << byte2BitLen) | portion2;

            ret = (byte) (combined & 0xFF);
        }

        return ret;
    }

    // Why work on longs? It may be more efficient to have the byte[] containing the bitstring be a long[].
    private static long isolateBitsToBottom(long data, int offset, int len) {
        Validate.isTrue(offset >= 0);
        Validate.isTrue(len >= 0);
        Validate.isTrue(offset + len <= 64);

        long mask = (1L << len) - 1L;

        return (data >>> offset) & mask;
    }

    private static int calculateRequiredByteArraySize(int bitLength) {
        Validate.inclusiveBetween(0, Integer.MAX_VALUE, bitLength);

        int fullByteCount = bitLength / 8;
        int remainingBits = bitLength % 8;

        int byteLength = fullByteCount + (remainingBits == 0 ? 0 : 1);

        return byteLength;
    }

    /**
     * Get the number of bits that are that this bitstring's prefix shares with another bitstring.
     * @param other other bitstring to test against
     * @return number of common prefix bits
     * @throws NullPointerException if any argument is {@code null}
     */
    public int getSharedPrefixLength(BitString other) {
        Validate.notNull(other);

        int maxCompareLenAsBits = Math.min(bitLength, other.bitLength);
        int maxCompareLenAsBytes = calculateRequiredByteArraySize(maxCompareLenAsBits);

        int nextByteIdx = 0;
        for (int i = 0; i < maxCompareLenAsBytes; i++) {
            if (other.data[i] != data[i]) {
                break;
            }
            nextByteIdx++;
        }

        if (nextByteIdx == maxCompareLenAsBytes) {
            // All bytes matched, both string prefixes match entirely
            return maxCompareLenAsBits;
        }

        int thisLastByte = data[nextByteIdx] & 0xFF;
        int otherLastByte = other.data[nextByteIdx] & 0xFF;

        int bitMatchCount = 0;
        int remainingBits = maxCompareLenAsBits - (nextByteIdx * 8);
        for (int i = 0; i < remainingBits; i++) {
            int thisBit = (thisLastByte >> i) & 0x01;
            int otherBit = (otherLastByte >> i) & 0x01;
            if (thisBit != otherBit) {
                break;
            }
            bitMatchCount++;
        }

        int finalBitMatchCount = (nextByteIdx * 8) + bitMatchCount;

        return finalBitMatchCount;
    }

    /**
     * Get the number of bits that are that this bitstring's suffix shares with another bitstring.
     * @param other other bitstring to test against
     * @return number of common suffix bits
     * @throws NullPointerException if any argument is {@code null}
     */
    public int getSharedSuffixLength(BitString other) {
        Validate.notNull(other);

        // You can make this more efficient by doing something similar to getSharedPrefixLength()

        int thisBitOffset = bitLength - 1;
        int otherBitOffset = other.bitLength - 1;
        int bitMatchCount = 0;
        while (thisBitOffset >= 0 && otherBitOffset >= 0) {
            boolean thisBit = getBit(thisBitOffset);
            boolean otherBit = other.getBit(otherBitOffset);
            if (thisBit != otherBit) {
                break;
            }
            bitMatchCount++;
            thisBitOffset--;
            otherBitOffset--;
        }

        return bitMatchCount;
    }

    /**
     * Get bit from this bitstring.
     * @param offset offset of bit
     * @return {@code true} if bit is 1, {@code false} if bit is 0
     * @throws IllegalArgumentException if {@code offset < 0} or if {@code offset > bitLength}
     */
    public boolean getBit(int offset) {
        Validate.isTrue(offset >= 0);
        Validate.isTrue(offset < bitLength);

        int bitPos = offset % 8;
        int bytePos = offset / 8;

        int bitMask = 1 << bitPos;
        return (data[bytePos] & bitMask) != 0;
    }

    /**
     * Set bit within a copy of this bitstring.
     * @param offset offset of bit
     * @param bit {@code true} if bit is 1, {@code false} if bit is 0
     * @return new ID that has bit set
     * @throws IllegalArgumentException if {@code offset < 0} or if {@code offset > bitLength}
     */
    public BitString setBit(int offset, boolean bit) {
        Validate.isTrue(offset >= 0);
        Validate.isTrue(offset < bitLength);

        byte[] dataCopy = Arrays.copyOf(data, data.length);

        int bitPos = offset % 8;
        int bytePos = offset / 8;

        if (bit) {
            int bitMask = 1 << bitPos;
            dataCopy[bytePos] |= bitMask;
        } else {
            int bitMask = ~(1 << bitPos);
            dataCopy[bytePos] &= bitMask;
        }

        return new BitString(dataCopy, bitLength);
    }

    /**
     * Flip bit within a copy of this bitstring.
     * @param offset offset of bit
     * @return new bitstring that has bit flipped
     * @throws IllegalArgumentException if {@code offset < 0} or if {@code offset > bitLength}
     */
    public BitString flipBit(int offset) {
        Validate.isTrue(offset >= 0);
        Validate.isTrue(offset < bitLength);

        boolean bit = getBit(offset);
        return setBit(offset, !bit);
    }

    /**
     * Get multiple bits from this bitstring.
     * @param offset offset of bit within this bitstring to read from
     * @param len number of bits to get
     * @return bits starting from {@code offset} to {@code offset + len} from this bitstring
     * @throws IllegalArgumentException if {@code offset < 0} or if {@code offset > bitLength} or
     * {@code offset + other.bitLength > bitLength}
     */
    public BitString getBits(int offset, int len) {
        Validate.isTrue(offset >= 0);
        Validate.isTrue(offset <= this.bitLength);
        int end = offset + len;
        Validate.isTrue(end <= this.bitLength);

        int lenAsBytes = calculateRequiredByteArraySize(len);
        byte[] dataCopy = new byte[lenAsBytes];

        // TODO: You can make this much more efficient
        for (int i = 0; i < len; i++) {
            int readIdx = offset + i;
            int readBitPos = readIdx % 8;
            int readBytePos = readIdx / 8;

            int readBitMask = 1 << readBitPos;
            boolean bit = (data[readBytePos] & readBitMask) != 0;

            int writeIdx = i;
            int writeBitPos = writeIdx % 8;
            int writeBytePos = writeIdx / 8;

            if (bit) {
                int bitMask = 1 << writeBitPos;
                dataCopy[writeBytePos] |= bitMask;
            } else {
                int bitMask = ~(1 << writeBitPos);
                dataCopy[writeBytePos] &= bitMask;
            }
        }

        return new BitString(dataCopy, len);
    }

    /**
     * Get multiple bits from this bitstring as a long.
     * <p>
     * For example {@code BitString.createFromNumber(0x3CFA000000000000L, 0, 16)} will generate the bit string {@code 0011 1100 0101 1111},
     * which if you called {@code getBitsAsLong(8, 4)} on would generate the long {@code 5L} ({@code 5L = 0x0101L}).
     * @param offset offset of bit within this bitstring to read from
     * @param len number of bits to get
     * @return bits starting from {@code offset} to {@code offset + len} from this bitstring, inside of a long
     * @throws IllegalArgumentException if {@code offset < 0} or if {@code offset > bitLength} or if {@code offset + len > bitLength} or if
     * {@code len > 64}
     */
    public long getBitsAsLong(int offset, int len) {
        Validate.isTrue(offset >= 0);
        Validate.isTrue(offset <= this.bitLength);
        int end = offset + len;
        Validate.isTrue(end <= this.bitLength);
        Validate.isTrue(len <= 64);

        long dataCopy = 0;

        // TODO: You can make this much more efficient
        for (int i = 0; i < len; i++) {
            int readIdx = offset + i;
            int readBitPos = readIdx % 8;
            int readBytePos = readIdx / 8;

            int readBitMask = 1 << readBitPos;
            boolean bit = (data[readBytePos] & readBitMask) != 0;

            int writeBitPos = (len - i) - 1;

            if (bit) {
                long bitMask = 1L << writeBitPos;
                dataCopy |= bitMask;
            } else {
                long bitMask = ~(1L << writeBitPos);
                dataCopy &= bitMask;
            }
        }

        return dataCopy;
    }

    /**
     * Set multiple bits within a copy of this bitstring.
     * @param offset offset of bit within this bitstring to write to
     * @param other bits to set
     * @return new bitstring that has bit set
     * @throws NullPointerException if any argument is {@code null}
     * @throws IllegalArgumentException if {@code offset < 0} or if {@code offset > bitLength} or
     * {@code offset + other.bitLength > bitLength}
     */
    public BitString setBits(int offset, BitString other) {
        Validate.notNull(other);
        Validate.isTrue(offset >= 0);
        Validate.isTrue(offset <= bitLength);
        int end = offset + other.bitLength;
        Validate.isTrue(end <= bitLength);

        byte[] dataCopy = Arrays.copyOf(data, data.length);

        // TODO: You can make this much more efficient
        for (int i = offset; i < end; i++) {
            int bitPos = i % 8;
            int bytePos = i / 8;

            if (other.getBit(i - offset)) {
                int bitMask = 1 << bitPos;
                dataCopy[bytePos] |= bitMask;
            } else {
                int bitMask = ~(1 << bitPos);
                dataCopy[bytePos] &= bitMask;
            }
        }

        return new BitString(dataCopy, bitLength);
    }

    /**
     * Append bits to a copy of this bitstring.
     * @param other bits to append
     * @return new bitstring that has bit set
     * @throws NullPointerException if any argument is {@code null}
     */
    public BitString appendBits(BitString other) {
        Validate.notNull(other);

        int offset = bitLength;
        int end = offset + other.bitLength;

        int arrLen = calculateRequiredByteArraySize(end);

        byte[] dataCopy = Arrays.copyOf(data, arrLen);

        // TODO: You can make this much more efficient
        for (int i = offset; i < end; i++) {
            int bitPos = i % 8;
            int bytePos = i / 8;

            if (other.getBit(i - offset)) {
                int bitMask = 1 << bitPos;
                dataCopy[bytePos] |= bitMask;
            } else {
                int bitMask = ~(1 << bitPos);
                dataCopy[bytePos] &= bitMask;
            }
        }

        return new BitString(dataCopy, end);
    }

    /**
     * Gets the maximum bit length for this bitstring.
     * @return max bit length for bitstring
     */
    public int getBitLength() {
        return bitLength;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 89 * hash + Arrays.hashCode(this.data);
        hash = 89 * hash + this.bitLength;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final BitString other = (BitString) obj;
        if (!Arrays.equals(this.data, other.data)) {
            return false;
        }
        if (this.bitLength != other.bitLength) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder(bitLength + 13); //13 = '(' + max characters an int can be when converted to string + ')' + ' '
        //        sb.append('(').append(bitLength).append(") ");
        for (int i = 0; i < bitLength; i++) {
            //            if (i % 4 == 0 && i != 0) {
            //                sb.append(' ');
            //            }
            sb.append(getBit(i) /*== true*/ ? 1 : 0);
        }
        return sb.toString();
    }
}