JavaScript
JavaScriptCore
Swift
16進文字列
bigint

【Swift4.0】16進数文字列をbigintに変換【JavaScriptCore】

ある案件で16進数文字列をbigintに変換する必要に迫られました。Swiftでbigintを扱えるライブラリはあるにはあるものの、16進数文字列から変換できるものはOpenSSLを使用するものだったりしてインストールが少々メンドくさそう…何かいい手はないかと考えた結果、とある方の書いたJavascriptをJavaScriptCore経由で利用させていただくことにしました。

プロジェクト導入手順

1. hex2decをgithubからダウンロード

ダウンロードしたindex.htmlを開いてみると分かりますがWebツールです。これのjs部分を利用させてもらいます。

lokielse/hex2dec
hex1.png

2. jsファイルを作成

上記のindex.htmlをエディタで開き、ソースからhtmlタグとjsの実行部分を除きます。jsの定義部分のみとなったファイルを今回は bigint.jsと命名して保存しました。

bigint.js

/**
 * A function for converting hex <-> dec w/o loss of precision.
 *
 * The problem is that parseInt("0x12345...") isn't precise enough to convert
 * 64-bit integers correctly.
 *
 * Internally, this uses arrays to encode decimal digits starting with the least
 * significant:
 * 8 = [8]
 * 16 = [6, 1]
 * 1024 = [4, 2, 0, 1]
 */

// Adds two arrays for the given base (10 or 16), returning the result.
// This turns out to be the only "primitive" operation we need.
function add(x, y, base) {
    var z = []
    var n = Math.max(x.length, y.length)
    var carry = 0
    var i = 0
    while (i < n || carry) {
        var xi = i < x.length ? x[i] : 0
        var yi = i < y.length ? y[i] : 0
        var zi = carry + xi + yi
        z.push(zi % base)
        carry = Math.floor(zi / base)
        i++
    }
    return z
}

// Returns a*x, where x is an array of decimal digits and a is an ordinary
// JavaScript number. base is the number base of the array x.
function multiplyByNumber(num, x, base) {
    if (num < 0) return null
    if (num == 0) return []

    var result = []
    var power = x
    while (true) {
        if (num & 1) {
            result = add(result, power, base)
        }
        num = num >> 1
        if (num === 0) break
        power = add(power, power, base)
    }

    return result
}

function parseToDigitsArray(str, base) {
    var digits = str.split('')
    var ary = []
    for (var i = digits.length - 1; i >= 0; i--) {
        var n = parseInt(digits[i], base)
        if (isNaN(n)) return null
        ary.push(n)
    }
    return ary
}

function convertBase(str, fromBase, toBase) {
    var digits = parseToDigitsArray(str, fromBase)
    if (digits === null) return null

    var outArray = []
    var power = [1]
    for (var i = 0; i < digits.length; i++) {
        // invariant: at this point, fromBase^i = power
        if (digits[i]) {
            outArray = add(outArray, multiplyByNumber(digits[i], power, toBase), toBase)
        }
        power = multiplyByNumber(fromBase, power, toBase)
    }

    var out = ''
    for (var i = outArray.length - 1; i >= 0; i--) {
        out += outArray[i].toString(toBase)
    }
    return out
}

function decToHex(decStr) {
    var hex = convertBase(decStr, 10, 16)
    return hex ? '0x' + hex : null
}

function hexToDec(hexStr) {
    if (hexStr.substring(0, 2) === '0x') hexStr = hexStr.substring(2)
    hexStr = hexStr.toLowerCase()
    return convertBase(hexStr, 16, 10)
}

3. jsファイルをプロジェクトに追加

ターゲット選択 -> Build Phase -> Copy Bundle Resources と辿り追加します。
hex2.png

4. JavaScriptCore.frameworkをプロジェクトに追加

ターゲット選択 -> General -> Linked Frameworks and Libraries と辿り追加します。
hex3.png

5. 使う

準備完了です。16進数文字列をbigintの10進数文字列に変換するサンプルコードです。

冒頭でJavaScriptCore.frameworkをインポートします。

import JavaScriptCore
        // bigint.jsを文字列バッファ上に取得

        let bundle = Bundle.main.path(forResource: "bigint", ofType: "js")
        var buff = try! String(contentsOfFile: bundle!, encoding: String.Encoding.utf8)

        let hexStr = "0x15bcebe43a644c5400000000000" // for test

        // 定義バッファに「var ret = hexToDec('0x15bcebe43a644c5400000000000');」をアペンド
        buff += "var ret = hexToDec('" + hexStr + "');"

        // jsコンテキスト生成
        let context = JSContext()!

        // 文字列をコードとして評価 (js's eval)
        context.evaluateScript(buff)

        // 評価後のretを取得して文字列に変換 (JSValue -> String)
        let value = context.objectForKeyedSubscript("ret").toString()

        print("biginteger string => \(value!)")
        // デバッグコンソール表示結果: biginteger string => 27556156319844318314496000000000

bigint.jsを読み込んだバッファの末尾に実行コード文字列を追加しています。このサンプルでは16進数文字列をハードコーディングしていますが、実用時は変数hexStrをパラメタにとる関数を作成することになるでしょう。計算が必要なら10進数文字列からbigintをインスタンスできるライブラリはいくつか公開されていますのでそれらを利用すればよいと思います。

クラスメソッド化

    class func hexStringToDecimalString(hex:String) -> String {

        var value = ""

        do {
            let bundle = Bundle.main.path(forResource: "bigint", ofType: "js")
            var buff = try String(contentsOfFile: bundle!, encoding: String.Encoding.utf8)
            buff += "var ret = hexToDec('" + hex + "');"

            let context = JSContext()!
            context.evaluateScript(buff)
            value = context.objectForKeyedSubscript("ret").toString()

        } catch {
            //error
        }

        return value
    }

・・・読み込んだ定義部分に実行コードをアペンドするってのがちょっとしょっぱい感じですw
Swift/JSのインターフェースをカッコよく実装したいなら小飼さんのSwiftでJavaScriptの記事が参考になります。

ちなみに、bigでない普通のIntであればこれでOK。

Int("2a", radix: 16)! // => 42

補足 :bigintを扱えるSwiftライブラリ

BigInt
Swift-Big-Integer