Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: byte to hex string conversion optimizations #701

Merged
merged 12 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
package com.capacitorjs.community.plugins.bluetoothle

// Create a LUT for high performance ByteArray conversion
val HEX_LOOKUP_TABLE = IntArray(256) {
val hexChars = "0123456789ABCDEF"
val h: Int = (hexChars[(it shr 4)].code shl 8)
val l: Int = hexChars[(it and 0x0F)].code
(h or l)
}

fun bytesToString(bytes: ByteArray): String {
val stringBuilder = StringBuilder(bytes.size)
for (byte in bytes) {
// byte to hex string
stringBuilder.append(String.format("%02X ", byte))
// Custom implementation of ByteArray.toHexString until stdlib stabilizes
private fun ByteArray.toHexString(): String {
val result = CharArray(this.size * 2);
var i = 0;
for (byte in this) {
val hx = HEX_LOOKUP_TABLE[byte.toInt() and 0xFF]
result[i] = (hx shr 8).toChar()
result[i+1] = (hx and 0xFF).toChar()
i+=2
}
return stringBuilder.toString()
return result.concatToString()
}

fun bytesToString(bytes: ByteArray): String {
return bytes.toHexString()
}

fun stringToBytes(value: String): ByteArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class ConversionKtTest : TestCase() {
fun testBytesToString() {
val input = byteArrayOfInts(0xA1, 0x2E, 0x38, 0xD4, 0x89, 0xC3)
val output = bytesToString(input)
assertEquals("A1 2E 38 D4 89 C3 ", output)
assertEquals("A12E38D489C3", output)
}

fun testEmptyBytesToString() {
Expand Down
34 changes: 29 additions & 5 deletions ios/Plugin/Conversion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,36 @@ func descriptorValueToString(_ value: Any) -> String {
return ""
}

func dataToString(_ data: Data) -> String {
var valueString = ""
for byte in data {
valueString += String(format: "%02hhx ", byte)
extension Data {
func toHexString() -> String {
let hexChars = Array("0123456789abcdef".utf8)
if #available(iOS 14, *) {
return String(unsafeUninitializedCapacity: self.count*2) { (ptr) -> Int in
var strp = ptr.baseAddress!
for byte in self {
strp[0] = hexChars[Int(byte >> 4)]
strp[1] = hexChars[Int(byte & 0xF)]
strp += 2
}
return 2 * self.count
}
} else {
// Fallback implementation for iOS < 14, a bit slower
var result = ""
result.reserveCapacity(self.count * 2)
for byte in self {
let high = Int(byte >> 4)
let low = Int(byte & 0xF)
result.append(Character(UnicodeScalar(hexChars[high])))
result.append(Character(UnicodeScalar(hexChars[low])))
}
return result
}
}
return valueString
}

func dataToString(_ data: Data) -> String {
return data.toHexString()
}

func stringToData(_ dataString: String) -> Data {
Expand Down
12 changes: 6 additions & 6 deletions ios/PluginTests/ConversionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class ConversionTests: XCTestCase {
func testDataToString() throws {
let input = Data([0xA1, 0x2E, 0x38, 0xD4, 0x89, 0xC3])
let output = dataToString(input)
XCTAssertEqual(output, "a1 2e 38 d4 89 c3 ")
XCTAssertEqual(output, "a12e38d489c3")
}

func testStringToData() throws {
Expand Down Expand Up @@ -44,11 +44,11 @@ class ConversionTests: XCTestCase {
}

func testDescriptorValueToString() throws {
XCTAssertEqual(descriptorValueToString("Hello"), "48 65 6c 6c 6f ")
XCTAssertEqual(descriptorValueToString(Data([0, 5, 255])), "00 05 ff ")
XCTAssertEqual(descriptorValueToString(UInt16(258)), "02 01 ")
XCTAssertEqual(descriptorValueToString(UInt16(1)), "01 00 ")
XCTAssertEqual(descriptorValueToString(NSNumber(1)), "01 00 ")
XCTAssertEqual(descriptorValueToString("Hello"), "48656c6c6f")
XCTAssertEqual(descriptorValueToString(Data([0, 5, 255])), "0005ff")
XCTAssertEqual(descriptorValueToString(UInt16(258)), "0201")
XCTAssertEqual(descriptorValueToString(UInt16(1)), "0100")
XCTAssertEqual(descriptorValueToString(NSNumber(1)), "0100")
XCTAssertEqual(descriptorValueToString(0), "")
}

Expand Down
9 changes: 9 additions & 0 deletions src/conversion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ describe('hexStringToDataView', () => {
expect(result.getUint8(2)).toEqual(200);
});

it('should work without spaces', () => {
const value = '0005C8';
const result = hexStringToDataView(value);
expect(result.byteLength).toEqual(3);
expect(result.getUint8(0)).toEqual(0);
expect(result.getUint8(1)).toEqual(5);
expect(result.getUint8(2)).toEqual(200);
});

it('should convert an empty hex string to a DataView', () => {
const value = '';
const result = hexStringToDataView(value);
Expand Down
29 changes: 22 additions & 7 deletions src/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,28 @@ export function numberToUUID(value: number): string {
return `0000${value.toString(16).padStart(4, '0')}-0000-1000-8000-00805f9b34fb`;
}

export function hexStringToDataView(value: string): DataView {
const numbers: number[] = value
.trim()
.split(' ')
.filter((e) => e !== '')
.map((s) => parseInt(s, 16));
return numbersToDataView(numbers);
/**
* Convert a string of hex into a DataView of raw bytes.
* Note: characters other than [0-9a-fA-F] are ignored
* @param hex string of values, e.g. "00 01 02" or "000102"
* @return DataView of raw bytes
*/
export function hexStringToDataView(hex: string): DataView {
const bin = [];
let i,
c,
isEmpty = 1,
buffer = 0;
for (i = 0; i < hex.length; i++) {
c = hex.charCodeAt(i);
if ((c > 47 && c < 58) || (c > 64 && c < 71) || (c > 96 && c < 103)) {
buffer = (buffer << 4) ^ ((c > 64 ? c + 9 : c) & 15);
if ((isEmpty ^= 1)) {
bin.push(buffer & 0xff);
}
}
}
return numbersToDataView(bin);
}

export function dataViewToHexString(value: DataView): string {
Expand Down
Loading