base62.jsをTypedCoffeeScriptで書き直してみたメモ

自作のモジュール、[base62.js ver0.4.1](https://github.com/sasaplus1/base62.js/tree/c91c5978169f90831047c86ae2ea8876e6c1e56f)をTypedCoffeeScriptで書き直してみました。

## セットアップ

とりあえずTypedCoffeeScriptをインストールするので、package.jsonにtyped-coffee-scriptを追加します。ついでにnpm-scriptを実行するとコンパイルできるようにしておきます。

```diff
commit cbfc1b149e023cea1407f21cf56941cc0f216e3c
Author: sasaplus1 <sasaplus1@gmail.com>
Date:   Sat Nov 23 14:29:26 2013 +0900

    appended base62.tc.js

diff --git a/package.json b/package.json
index f434cfe..4de1497 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
   },
   "scripts": {
     "bower": "bower install",
+    "c": "tcoffee --bare --js --output ./base62.tc.js --input ./coffee/base62.coffee",
     "fix": "fixjsstyle --strict --nojsdoc -r ./lib -r ./test ./index.js",
     "lint": "gjslint --strict --nojsdoc -r ./lib -r ./test ./index.js",
     "mini": "uglifyjs ./lib/base62.js --comments -cm -d 'module=void 0' -r base62 -o ./base62.min.js",
@@ -24,6 +25,7 @@
     "expect.js": "~0.2",
     "mocha": "~1.14",
     "testem": "~0.5",
+    "typed-coffee-script": "~0.8",
     "uglify-js": "~2.4"
   }
 }
```

```bash
$ mkdir ./coffee
$ touch ./coffee/base62.coffee
$ npm i
```

これで確かセットアップは大丈夫。コンパイルするときは

```bash
$ npm run-script c
```

です。

## コードを書く

### coffee/base62.coffee

```coffeescript
do (global :: Object = this) ->

  isInteger :: Number -> Boolean = Number.isInteger or (value :: Number) ->
    isFinite(value) and value is Math.floor(value)

  createConverter :: String -> Base62 = (table :: String) ->
    base62 :: Base62 = if arguments.length <= 0 then new Base62 else new Base62(table)
    #base62.createConverter :: String -> Base62 = createConverter
    ##base62.createConverter :: String -> Base62
    ##base62.createConverter = createConverter
    base62.createConverter = createConverter
    base62

  class Base62

    table_ :: String
    createConverter :: String -> Base62

    constructor: (table :: String) ->
      if arguments.length <= 0
        table = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
      else if table.length isnt 62
        throw new Error 'parameter must be 62 chars'
      @table_ = table

    decode: (str :: String) ->
      throw new Error "parameter is unsupported format: #{str}" if not /^-?[\da-zA-Z]+$/.test str

      isNegative :: Boolean = str[0] is '-'
      str = str.slice 1 if isNegative

      table :: String = @table_

      decodedNum :: Int = 0
      i :: Int = str.length
      len :: Int = str.length

      while i--
        decodedNum += table.indexOf(str[i]) * Math.pow 62, len - i - 1

      if isNegative then decodedNum *= -1 else decodedNum

    encode: (num :: Int) ->
      throw new Error "parameter is not an integer: #{num}" if not isInteger num

      return '0' if num is 0

      isNegative :: Boolean = num < 0
      num = Math.abs num

      table :: String = @table_
      encodedStr :: String = ''

      while num > 0
        encodedStr = table.charAt(num % 62) + encodedStr
        num = Math.floor num / 62

      if isNegative then "-#{encodedStr}" else encodedStr

  if module?
    module.exports ###:: Base62### = createConverter()
    if process.env.NODE_ENV is 'test'
      module.exports.Base62_ = Base62
      module.exports.isInteger_ = isInteger
  else
    global.base62 ###:: Base62### = createConverter()
    if Mocha?
      global.base62.Base62_ = Base62
      global.base62.isInteger_ = isInteger

  return
```

こんな感じでしょうか。勘違いしてるところもままあるような。

さすがはCoffeeScriptで、コード量がだいぶ削減されたと思います。
型の無い生活に慣れたので意外と辛かった……
あと初めてCoffeeScriptでクラスを書いたような。

## コンパイルしてみる

```bash
$ npm run-script c
```

でコンパイルしたJavaScriptが以下です。

```js
// Generated by CoffeeScript 0.8.3
(function (global) {
  var Base62, createConverter, isInteger;
  isInteger = Number.isInteger || function (value) {
    return isFinite(value) && value === Math.floor(value);
  };
  createConverter = function (table) {
    var base62;
    base62 = arguments.length <= 0 ? new Base62 : new Base62(table);
    base62.createConverter = createConverter;
    return base62;
  };
  Base62 = function () {
    function Base62(table) {
      if (arguments.length <= 0) {
        table = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
      } else if (table.length !== 62) {
        throw new Error('parameter must be 62 chars');
      }
      this.table_ = table;
    }
    Base62.prototype.decode = function (str) {
      var decodedNum, i, isNegative, len, table;
      if (!/^-?[\da-zA-Z]+$/.test(str))
        throw new Error('parameter is unsupported format: ' + str);
      isNegative = str[0] === '-';
      if (isNegative)
        str = str.slice(1);
      table = this.table_;
      decodedNum = 0;
      i = str.length;
      len = str.length;
      while (i--) {
        decodedNum += table.indexOf(str[i]) * Math.pow(62, len - i - 1);
      }
      if (isNegative) {
        return decodedNum *= -1;
      } else {
        return decodedNum;
      }
    };
    Base62.prototype.encode = function (num) {
      var encodedStr, isNegative, table;
      if (!isInteger(num))
        throw new Error('parameter is not an integer: ' + num);
      if (num === 0)
        return '0';
      isNegative = num < 0;
      num = Math.abs(num);
      table = this.table_;
      encodedStr = '';
      while (num > 0) {
        encodedStr = table.charAt(num % 62) + encodedStr;
        num = Math.floor(num / 62);
      }
      if (isNegative) {
        return '-' + encodedStr;
      } else {
        return encodedStr;
      }
    };
    return Base62;
  }();
  if ('undefined' !== typeof module && null != module) {
    module.exports = createConverter();
    if (process.env.NODE_ENV === 'test') {
      module.exports.Base62_ = Base62;
      module.exports.isInteger_ = isInteger;
    }
  } else {
    global.base62 = createConverter();
    if ('undefined' !== typeof Mocha && null != Mocha) {
      global.base62.Base62_ = Base62;
      global.base62.isInteger_ = isInteger;
    }
  }
}(this));
```

## ユニットテスト

ついでにユニットテストに通してみたり。

```diff
commit cbfc1b149e023cea1407f21cf56941cc0f216e3c
Author: sasaplus1 <sasaplus1@gmail.com>
Date:   Sat Nov 23 14:29:26 2013 +0900

    appended base62.tc.js

diff --git a/test/test-base62.js b/test/test-base62.js
index 2e1dff3..8e3716d 100644
--- a/test/test-base62.js
+++ b/test/test-base62.js
@@ -2,7 +2,7 @@ var expect, base62;
 
 if (typeof module !== 'undefined') {
   expect = require('expect.js');
-  base62 = require('../');
+  base62 = require('../base62.tc.js');
 } else {
   expect = this.expect;
   base62 = this.base62;
```

例外が投げられるかどうかのテストは当然失敗しますが、それ以外はちゃんと通ってました。

```bash
$ npm ts

> base62.js@0.4.1 test /Users/sasaplus1/Repos/git/base62.coffee
> NODE_ENV=test mocha



  base62
    #createConverter()
      ✓ should return Base62 instance
    #decode()
      ✓ should throw error if parameter is not a integer
      1) should throw error if parameter type is not a string
      ✓ should convert to integer from base62 string
    #encode()
      ✓ should throw error if parameter is not a integer
      2) should throw error if parameter type is not a number
      ✓ should convert to base62 string from integer
    #Base62()
      3) should throw error if parameter is not a string
      ✓ should throw error if parameter is not 62 chars
      ✓ should has default table
      ✓ should set another table
    #isInteger()
      ✓ should return false if parameter is not a number
      ✓ should return false if parameter is not a finite number
      ✓ should return false if parameter is floating-point number
      ✓ should return true if parameter is integer

  index
    ✓ should export some functions


  13 passing (105ms)
  3 failing

  1) base62 #decode() should throw error if parameter type i  1) base62 #decode() should throw errn to throw an exception
      at Assertion.assert (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/expect.js/expect.js:99:13)
      at Assertion.throwError.Assertion.throwException (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/expect.js/expect.js:155:10)
      at Context.<anonymous> (/Users/sasaplus1/Repos/git/base62.coffee/test/test-base62.js:50:23)
      at Test.Runnable.run (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runnable.js:211:32)
      at Runner.runTest (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:358:10)
      at /Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:404:12
      at next (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:284:14)
      at /Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:293:7
      at next (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:237:23)
      at Object._onImmediate (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:261:5)
      at processImmediate [as _immediateCallback] (timers.js:330:15)

  2) base62 #encode() should throw error if parameter type is not a number:
     Error: expected {} to be an instance of TypeError
      at Assertion.assert (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/expect.js/expect.js:99:13)
      at Assertion.a.Assertion.an [as a] (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/expect.js/expect.js:261:12)
      at Function.a (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/expect.js/expect.js:480:17)
      at fn (/Users/sasaplus1/Repos/git/base62.coffee/test/test-base62.js:97:25)
      at Assertion.throwError.Assertion.throwException (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/expect.js/expect.js:136:9)
      at Context.<anonymous> (/Users/sasaplus1/Repos/git/base62.coffee/test/test-base62.js:100:25)
      at Test.Runnable.run (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runnable.js:211:32)
      at Runner.runTest (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:358:10)
      at /Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:404:12
      at next (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:284:14)
      at /Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:293:7
      at next (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:237:23)
      at Object._onImmediate (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:261:5)
      at processImmediate [as _immediateCallback] (timers.js:330:15)

  3) base62 #Base62() should throw error if parameter is not a string:
     Error: expected {} to be an instance of TypeError
      at Assertion.assert (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/expect.js/expect.js:99:13)
      at Assertion.a.Assertion.an [as a] (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/expect.js/expect.js:261:12)
      at Function.a (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/expect.js/expect.js:480:17)
      at fn (/Users/sasaplus1/Repos/git/base62.coffee/test/test-base62.js:130:25)
      at Assertion.throwError.Assertion.throwException (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/expect.js/expect.js:136:9)
      at Context.<anonymous> (/Users/sasaplus1/Repos/git/base62.coffee/test/test-base62.js:133:23)
      at Test.Runnable.run (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runnable.js:211:32)
      at Runner.runTest (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:358:10)
      at /Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:404:12
      at next (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:284:14)
      at /Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:293:7
      at next (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:237:23)
      at Object._onImmediate (/Users/sasaplus1/Repos/git/base62.coffee/node_modules/mocha/lib/runner.js:261:5)
      at processImmediate [as _immediateCallback] (timers.js:330:15)



npm ERR! weird error 3
npm ERR! not ok code 0
```

## $ git show

```diff
commit cbfc1b149e023cea1407f21cf56941cc0f216e3c
Author: sasaplus1 <sasaplus1@gmail.com>
Date:   Sat Nov 23 14:29:26 2013 +0900

    appended base62.tc.js

diff --git a/base62.tc.js b/base62.tc.js
new file mode 100644
index 0000000..5d4500d
--- /dev/null
+++ b/base62.tc.js
@@ -0,0 +1,77 @@
+// Generated by CoffeeScript 0.8.3
+(function (global) {
+  var Base62, createConverter, isInteger;
+  isInteger = Number.isInteger || function (value) {
+    return isFinite(value) && value === Math.floor(value);
+  };
+  createConverter = function (table) {
+    var base62;
+    base62 = arguments.length <= 0 ? new Base62 : new Base62(table);
+    base62.createConverter = createConverter;
+    return base62;
+  };
+  Base62 = function () {
+    function Base62(table) {
+      if (arguments.length <= 0) {
+        table = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+      } else if (table.length !== 62) {
+        throw new Error('parameter must be 62 chars');
+      }
+      this.table_ = table;
+    }
+    Base62.prototype.decode = function (str) {
+      var decodedNum, i, isNegative, len, table;
+      if (!/^-?[\da-zA-Z]+$/.test(str))
+        throw new Error('parameter is unsupported format: ' + str);
+      isNegative = str[0] === '-';
+      if (isNegative)
+        str = str.slice(1);
+      table = this.table_;
+      decodedNum = 0;
+      i = str.length;
+      len = str.length;
+      while (i--) {
+        decodedNum += table.indexOf(str[i]) * Math.pow(62, len - i - 1);
+      }
+      if (isNegative) {
+        return decodedNum *= -1;
+      } else {
+        return decodedNum;
+      }
+    };
+    Base62.prototype.encode = function (num) {
+      var encodedStr, isNegative, table;
+      if (!isInteger(num))
+        throw new Error('parameter is not an integer: ' + num);
+      if (num === 0)
+        return '0';
+      isNegative = num < 0;
+      num = Math.abs(num);
+      table = this.table_;
+      encodedStr = '';
+      while (num > 0) {
+        encodedStr = table.charAt(num % 62) + encodedStr;
+        num = Math.floor(num / 62);
+      }
+      if (isNegative) {
+        return '-' + encodedStr;
+      } else {
+        return encodedStr;
+      }
+    };
+    return Base62;
+  }();
+  if ('undefined' !== typeof module && null != module) {
+    module.exports = createConverter();
+    if (process.env.NODE_ENV === 'test') {
+      module.exports.Base62_ = Base62;
+      module.exports.isInteger_ = isInteger;
+    }
+  } else {
+    global.base62 = createConverter();
+    if ('undefined' !== typeof Mocha && null != Mocha) {
+      global.base62.Base62_ = Base62;
+      global.base62.isInteger_ = isInteger;
+    }
+  }
+}(this));
diff --git a/coffee/base62.coffee b/coffee/base62.coffee
new file mode 100644
index 0000000..04cd6aa
--- /dev/null
+++ b/coffee/base62.coffee
@@ -0,0 +1,71 @@
+do (global :: Object = this) ->
+
+  isInteger :: Number -> Boolean = Number.isInteger or (value :: Number) ->
+    isFinite(value) and value is Math.floor(value)
+
+  createConverter :: String -> Base62 = (table :: String) ->
+    base62 :: Base62 = if arguments.length <= 0 then new Base62 else new Base62(table)
+    #base62.createConverter :: String -> Base62 = createConverter
+    ##base62.createConverter :: String -> Base62
+    ##base62.createConverter = createConverter
+    base62.createConverter = createConverter
+    base62
+
+  class Base62
+
+    table_ :: String
+    createConverter :: String -> Base62
+
+    constructor: (table :: String) ->
+      if arguments.length <= 0
+        table = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
+      else if table.length isnt 62
+        throw new Error 'parameter must be 62 chars'
+      @table_ = table
+
+    decode: (str :: String) ->
+      throw new Error "parameter is unsupported format: #{str}" if not /^-?[\da-zA-Z]+$/.test str
+
+      isNegative :: Boolean = str[0] is '-'
+      str = str.slice 1 if isNegative
+
+      table :: String = @table_
+
+      decodedNum :: Int = 0
+      i :: Int = str.length
+      len :: Int = str.length
+
+      while i--
+        decodedNum += table.indexOf(str[i]) * Math.pow 62, len - i - 1
+
+      if isNegative then decodedNum *= -1 else decodedNum
+
+    encode: (num :: Int) ->
+      throw new Error "parameter is not an integer: #{num}" if not isInteger num
+
+      return '0' if num is 0
+
+      isNegative :: Boolean = num < 0
+      num = Math.abs num
+
+      table :: String = @table_
+      encodedStr :: String = ''
+
+      while num > 0
+        encodedStr = table.charAt(num % 62) + encodedStr
+        num = Math.floor num / 62
+
+      if isNegative then "-#{encodedStr}" else encodedStr
+
+  if module?
+    module.exports ###:: Base62### = createConverter()
+    if process.env.NODE_ENV is 'test'
+      module.exports.Base62_ = Base62
+      module.exports.isInteger_ = isInteger
+  else
+    global.base62 ###:: Base62### = createConverter()
+    if Mocha?
+      global.base62.Base62_ = Base62
+      global.base62.isInteger_ = isInteger
+
+  return
diff --git a/package.json b/package.json
index f434cfe..4de1497 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
   },
   "scripts": {
     "bower": "bower install",
+    "c": "tcoffee --bare --js --output ./base62.tc.js --input ./coffee/base62.coffee",
     "fix": "fixjsstyle --strict --nojsdoc -r ./lib -r ./test ./index.js",
     "lint": "gjslint --strict --nojsdoc -r ./lib -r ./test ./index.js",
     "mini": "uglifyjs ./lib/base62.js --comments -cm -d 'module=void 0' -r base62 -o ./base62.min.js",
@@ -24,6 +25,7 @@
     "expect.js": "~0.2",
     "mocha": "~1.14",
     "testem": "~0.5",
+    "typed-coffee-script": "~0.8",
     "uglify-js": "~2.4"
   }
 }
diff --git a/test/test-base62.js b/test/test-base62.js
index 2e1dff3..8e3716d 100644
--- a/test/test-base62.js
+++ b/test/test-base62.js
@@ -2,7 +2,7 @@ var expect, base62;
 
 if (typeof module !== 'undefined') {
   expect = require('expect.js');
-  base62 = require('../');
+  base62 = require('../base62.tc.js');
 } else {
   expect = this.expect;
   base62 = this.base62;
```

## その他

そういえば文法よくわかってなくて、以下のように書いたら

```coffeescript
  isInteger = (value :: Number) Number.isInteger or (value :: Number) ->
    isFinite(value) and value is Math.floor(value)
```

以下のように出力されました。

```js
  isInteger = 3(Number.isInteger || function (value) {
    return isFinite(value) && value === Math.floor(value);
  });
```

3ってなんだろ……