ExtendScriptで演算子のオーバーロード

# 演算子のオーバーロード

会社で演算子のオーバーロードの話になったのですが、ExtendScriptでは演算子のオーバーロードがサポートされていたので、試してみました。

## アニメならではの時間表記

アニメではカット等の時間表記が‟3+18”のように秒と、コマ数で表記されるのが一般的です。
1秒あたりのコマ数は24コマとなっているので、この場合コマ数でいうと90コマになります。
また、コマ数同士の計算もよく行われます。

例えば‟3+18”のカットと‟1+12”のカットの時間の合計は‟5+6”です、アニメ撮影マンは24進数がわかるよう訓練されているので簡単に暗算できますが、これをAfterEffectsのスクリプトでやろうとすると少し面倒です。

コマ数にしてしまえば普通に加算、減算ができるので以下のような関数を書けばいいでしょう。

```javascript
function frame2koma(frame, fps){
    var sec = Math.floor(frame / fps);
    var koma = frame % fps;
    return sec + '+' + koma;
    }

function koma2frame(koma, fps){
    var split_koma = koma.split('+');
    var sec = Number(split_koma[0]);
    var koma = Number(split_koma[1]);
    return sec * fps + koma;
    }

function add(operandA, operandB, fps){
    return frame2koma(koma2frame(operandA, fps) + koma2frame(operandB, fps), fps);
    }

var a = '3+18';
var b = '1+12';

var c = add(a, b, 24);

$.writeln(c); //'5+6'
```

これでも問題なく機能していますが、もっと直感的にa + bのように出来ないでしょうか。
そこでESTKでサポートされている演算子オーバーロードを使用してみます。

## ExtendScriptの演算子オーバーロードサポート

javaScript Tools Guide CC.pdfによると

>// define the constructor method
function MyClass (initialValue) {
 this.value = initialValue; 
} 
// define the addition operator 
MyClass.prototype [“+”] = function (operand) {
 return this.value + operand; 
} 
This allows you to perform the "+" operation with any object of this class:
var obj = new MyClass (5); 
Result: [object Object] obj + 10; 
Result: 15 

とのことです。

なるほど、prototypeに生やせばOKらしいですね。ということはクラスを作ってしまえばOKなのでゴリっとクラスを作って、そこで演算子をオーバーロードしていきます。

```javascript:KomaTime.jsxinc
var KomaTime = (function(){
    
    function KomaTime(initValue, fps){
        var frame = initValue;
        if (String(initValue).match(/\+/)){
            frame = koma2frame (initValue, fps);
            }
        this.frame = Number(frame);
        this.fps = fps;
        this.koma = frame2koma (frame, fps);
        }
    
    function frame2koma(frame, fps){
        var sign = (frame < 0) ? '-' : '';
        var sec = Math.floor(Math.abs(frame) / fps);
        var koma = Math.abs(frame) % fps;
        return sign + sec + '+' + koma;
        }
    
    function koma2frame(koma, fps){
        var split_koma = koma.split('+');
        var sec = Number(split_koma[0]);
        var koma = Number(split_koma[1]);
        if(sec < 0) return sec * fps - koma;
        return sec * fps + koma;
        }
    
    function correctOperand(operandValue, fps){
        var value = (operandValue instanceof KomaTime) ? String(operandValue) : operandValue;
        if (String(value).match(/\+/)){
            return koma2frame (value, fps);
            }
        return Number(value);
        }
    
    var p = KomaTime.prototype;
    
    p.toString = function(){
        return this.koma;
        }
    
    p ["+"] = function(operand){
        var corrected_Operand = correctOperand(operand, this.fps);
        var new_frame = this.frame + corrected_Operand;
        return new KomaTime(new_frame, this.fps);
        };
    
    p ["-"] = function(operand){
        var corrected_Operand = correctOperand(operand, this.fps);
        var new_frame = this.frame - corrected_Operand;
        return new KomaTime(new_frame, this.fps);
        };
    
    p ["<"] = function(operand){
        var corrected_Operand = correctOperand(operand, this.fps);
        if (this.frame < corrected_Operand) return true;
        return false;
        };
    
    p [">"] = function(operand){
        var corrected_Operand = correctOperand(operand, this.fps);
        if (this.frame > corrected_Operand) return true;
        return false;
        };
    
    p ["<="] = function(operand){
        var corrected_Operand = correctOperand(operand, this.fps);
        if (this.frame <= corrected_Operand) return true;
        return false;
        };
    
    p [">="] = function(operand){
        var corrected_Operand = correctOperand(operand, this.fps);
        if (this.frame >= corrected_Operand) return true;
        return false;
        };
    
    p ["=="] = function(operand){
        var corrected_Operand = correctOperand(operand, this.fps);
        if (this.frame == corrected_Operand) return true;
        return false;
        };
    
    p ["!="] = function(operand){
        var corrected_Operand = correctOperand(operand, this.fps);
        if (this.frame != corrected_Operand) return true;
        return false;
        };
    
    p ["==="] = function(operand){
        var corrected_Operand = correctOperand(operand, this.fps);
        if (this.frame === corrected_Operand) return true;
        return false;
        };
    
    p ["!=="] = function(operand){
        var corrected_Operand = correctOperand(operand, this.fps);
        if (this.frame !== corrected_Operand) return true;
        return false;
        };
    
    return KomaTime;
    
    }());
```
ExtendScriptにはassertがないので、簡単な比較でチェックしてみます。

```javascript:test.jsx
(function (){
    #include 'KomaTime.jsxinc'
    
    function assert(actual, expected) {
        $.writeln(actual === expected, '\tactual : ', String(actual), '\texpected : ', String(expected));
        }

    var kt1 = new KomaTime('3+18', 24);
    var kt2 = new KomaTime('1+12', 24);

    assert (kt1, '3+18');
    assert (kt2, '1+12');
    assert (kt1 + kt2, '5+6');
    assert (kt2 - kt1, '-2+6');
    assert ('-2+6' + kt1, kt2);
    assert (kt1 > 0, true);
    assert (kt1 < 0, false);
    assert (kt1 - kt2 > 0, true);
    assert (kt1 < 91, true);
    assert (kt1 <= 90, true);
    assert (kt1 >= 90, true);
    assert (kt1 == 90, true);
    assert (kt1 === 90, true);
    assert (kt2 != kt1, true);

}());
```

```terminal:JavaScriptコンソール
true	actual : 3+18	expected : 3+18
true	actual : 1+12	expected : 1+12
true	actual : 5+6	expected : 5+6
true	actual : -2+6	expected : -2+6
true	actual : 1+12	expected : 1+12
true	actual : true	expected : true
true	actual : false	expected : false
true	actual : true	expected : true
true	actual : true	expected : true
true	actual : true	expected : true
true	actual : true	expected : true
true	actual : true	expected : true
true	actual : true	expected : true
true	actual : true	expected : true
結果 : undefined
```

クラス自体のコードは長くなりましたが、これで加算だけではなく、減算、比較もできるようになったので、色々な計算が楽になりました。

オーバーロードできる演算子は以下のようです。
>Unary +, ~ 
Binary +, -,
*, /, %, ^ 
<, <=, == 
<<, >>, >>> 
&, |, ===

今回は乗算、除算、インクリメント、デクリメントはどう扱った方がいいのか考えて見送っているので、運用に合わせて機能を追加してみるのもいいと思います。

## まとめ

意外に便利。

## その他

ExtendScriptでクラスを書くときは、今は大体こんな感じで書いています。使用する際には即時関数化されたスコープ内で#includeすると、グローバル汚染もしないので#includeはコードの一番上に書かないようにしています。
ExtendScriptはES3準拠のAdobe独自言語ですが、標準でファイルアクセスができたり、簡単なSocketが実装されていたり、地味に面白い機能がいろいろあるのですが、ES3準拠のままアップデートがなく結構大変。とはいえAdobe製品の自動化には欠かせない機能です。