62進数を扱おう

昔、16進数というものを習ったときに、

「どうしてFまでしか使わないんだろう?」
「Zまで使えばもっと大きな値を短く表現できるのに」

と思ったものです。
そこで私は、62進数というものをObjective-Cのカテゴリってやつを使って、NSString, 及びNSNumberクラスでサポートすることを思い立ったわけです。

62進数とは

・10進数の0〜61までの数値を、'0' 〜 'Z'までの文字で表すことにします。
・0〜9までの数字の並びは10進数と同じです。
・9の次は、'a' とします。
・zの次は、'A' とします。
・10進数の62を、62進数で"10"とします。

※これら、数字 → 小文字 → 大文字の順の並びは、BASE64とは逆なので注意です。


62進数のメリット

・大きな数値を10進数で表記するよりも短い文字列で表現できます。

32ビットint値の最大値であれば、

10進数:10文字
62進数:6文字

で表現できます。


BASE64の話

数値(バイナリ)を文字列で表現するのには、BASE64というエンコード方式がよく使われます。数値を単に文字列で表現(送信、保存などのために)したいような場合には、ここでいう62進数ではなく、BASE64を使うべきでしょう。ここでいう62進数は、あくまで、単値の文字列としての表現方法の1つです。BASE64は、0〜63までの10進数値を1文字で表現することが可能ですが、BASE64を「64進数」と呼ぶには、少し無理があります。

なぜなら、

BASE64では、データを4バイト境界にパディングする
BASE64では、データが4の倍数バイトになります。
データの末尾が4の倍数バイトで終わらない場合は、不足分を '=' で埋めることになっています。

②62='+', 63='/'
61までは、数字とアルファベットで表現できますが、BASE64は、64という切りの良い表現範囲にするために、こともあろうか、+と/を使ってしまいました。そしてパディングには=が使われてしまっています。つまり、BASE64を64進数として使ってしまうと、

78+99/ZB

という文字の羅列を見たときに、これが数式を表しているのか、単なる1つの数値なのかが判断できなくなってしまうのです。



つうことで、実装してみます。
勉強用の課題としてもいいかな、と思ったり。
コメント多めにつけました。


base62.h

まあ、単純なC関数で十分なんですけど、せっかくだから私はこのObjective-Cのカテゴリってやつを使わせてもらうよ。

#import <Foundation/Foundation.h>

//
// 62進数
// 1文字で、10進数の0〜61までの数値を表現する。
// 0, 1, 2, ... 9, a, b, c, ... z, A, B, C, ... Z, 10, ...
//

// 注意1
// 16進数と比較して、アルファベットの大文字小文字を区別する点が異なります。

// 注意2
// BASE64と比較して、数字、小文字、大文字の並び順が逆です。



// 62進数対応 - 文字列クラス拡張
@interface NSString(Base62)
// 10進数 → 62進数文字列変換サポート
+(NSString*)base62StringWithInt:(int)value;
// 62進数文字列 → 10進数変換サポート
-(int)intValueAsBase62;
@end

// 62進数対応 - 数値クラス拡張
@interface NSNumber(Base62)
// 62進数文字列 → 10進数変換サポート
+(NSNumber*)numberWithBase62String:(NSString*)s;
// 10進数 → 62進数文字列変換サポート
-(NSString*)stringAsBase62;
@end

base62.m

多少速度を意識して、中身はC使ってます。

#import "base62.h"

// 10進数を1文字に変換するためのテーブル
const char tableForDecToBase62Char[62] = {
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
    'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
    'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D',
    'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
    'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
    'Y', 'Z'
};

// 1文字を10進数の数値に変換するためのテーブル
const char tableForBase62CharToDec[256] = {
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0,
    0,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,
    51,52,53,54,55,56,57,58,59,60,61,0, 0, 0, 0, 0,
    0,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,
    25,26,27,28,29,30,31,32,33,34,35,0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};

// 62進数対応 - 文字列クラス拡張
@implementation NSString(Base62)

// 10進数 → 62進数文字列変換サポート
+(NSString*)base62StringWithInt:(int)value {
    // 後のループの都合上、ゼロだけ特別扱い
    if (0 == value) {
        return @"0";
    }

    // 一時変数準備
    char chars[16];              // 15文字分のバッファを確保
    chars[15] = 0;               // 文字列の終端をマーク
    char* pos = chars + 15;      // ポインタを文字列末尾にセットアップ
    bool minus = false;          // 負の値だったら、最後に、先頭に '-' をつける
    
    // 負の値チェック
    if (0 > value) {
        minus = true;            // 負の値フラグを立てる
        value *= -1;             // 符号を反転して正の数にする
    }
    
    // 0になるまでvalueを62で割り続ける
    while (0 != value) {
        // 次の文字の格納位置にポインタをずらす
        -- pos;
        
        // valueを62で割った余りをテーブルから参照して1文字取得 → ポインタ位置にセット
        *pos = tableForDecToBase62Char[value % 62];
        
        // 62で割ることによって、次の桁へ移動
        value /= 62;
    }
    
    // 負の値だったら、先頭にマイナス符号をつける
    if (minus) {
        -- pos;
        *pos = '-';        // 顔文字じゃないよ!
    }

    // ASCII文字列をNSStringにして返却
    return [NSString stringWithUTF8String:pos];
}

// 62進数文字列 → 10進数変換サポート
-(int)intValueAsBase62 {
    // 一時変数準備
    int result = 0;                      // 計算結果の値を保持する変数を0で初期化
    const char* p = [self UTF8String];   // 文字列へのポインタを取得
    bool minus = false;                  // 負の値だったら、最後に符号反転する
    
    // 1文字目で符号チェック
    if ('-' == *p) {
        minus = true;                    // 負の値フラグを立てる
        ++ p;                            // 1文字目が数字じゃないので、次のポインタを次の文字位置へ
    }
    
    // 文字列の末尾まで、1桁繰り上がるごとに62倍していく
    while (0 != *p) {
        // 現在の値を繰り上げ
        result *= 62;
        
        // 1文字を数値に変換して加算する
        result += tableForBase62CharToDec[*p];
        
        // 次の文字位置へ
        ++ p;
    }
    
    // 負の値だったら符号を反転
    if (minus) {
        result *= -1;
    }
    
    // 演算結果を返却
    return result;
}

@end

// 62進数対応 - 数値クラス拡張
@implementation NSNumber(Base62)

// 62進数文字列 → 10進数変換サポート
+(NSNumber*)numberWithBase62String:(NSString*)s {
    return [NSNumber numberWithInt:[s intValueAsBase62]];
}

// 62進数文字列 → 10進数変換サポート
-(NSString*)stringAsBase62 {
    return [NSString base62StringWithInt:[self intValue]];
}

@end

使い方

int main(int argc, const char** argv)
{
     @autoreleasepool {
        NSString* s = [NSString base62StringWithInt:123456789];
        NSLog(@"%@", s);        //=> 8m0Kx

        int d = [s intValueAsBase62];
        NSLog(@"%d", d);        //=> 123456789
    }
}

まあ、ちっぽけなプログラムですが、なかなかよくできた気がします。

では、楽しい時間でした!
またの機会を!