プログラミング/CoffeeScript

(2912d) 更新


公開メモ

CoffeeScript

http://coffeescript.org/

もっと簡単に JavaScript を書きたい、という人向けに作られた言語だそうです。

最近の Ruby on Rails や Play Framework には自動的に JavaScript に変換してくれる機構が備わっているので シームレスに使うことができます

メリット

  • JavaScript より簡潔に書ける
  • JavaScript で気をつけなければならない点のいくつかを自動的にカバーしてくれる
  • まずまず書いてて気持ちいい

@ はクラススコープではちゃんとクラス名になる

例えば、

LANG:coffeescript
class Outer
   class @Inner

という書式で、ネストクラス Outer.Inner を定義可能

このときの @ は Outer.Inner に変換される。かしこい。

恐るべきことに、

LANG:coffeescript
class Outer
   class this.Inner

と書いても、Outer.Inner に変換される!なんと。

デメリット or 注意点

  • 普通の JavaScript が書きたくなくなる
  • 普通の for( ; ; ) が書けないのが痛いときがある
  • 最近はあまりはやっていないみたい???

for ... in ... と for ... of ... の見分けが付きにくい

JavaScript では for ... in ... は配列要素を列挙するための文法ではなく、 連想配列のキーを列挙するための文法なのですが、

http://d.hatena.ne.jp/amachang/20070202/1170386546

CoffeeScript では配列の列挙に使えるようになっています。

http://coffeescript.org/#loops

じゃ、連想配列のキーを列挙するにはどうするかというと、 for ... of ... なんだそうです。

わかりづらすぎる・・・

-> と => の違いも、気付きにくすぎ

まあ、エラーが出たら => に直すというスタンスで書いておけば、 読むときは気にせず読めるから良いのかもしれないけれど???

多重ループから脱出するためのラベル付き continue, break がない

https://groups.google.com/forum/#!msg/coffeescript/-LJ5OB2FNUw/UVrRdFwWdh8J

このあたりを読む限り、 CoffeeScript では JavaScript のラベルを実装する予定は無いみたいですね。

それじゃどうするかというと、
http://stackoverflow.com/questions/7655786/breaking-continuing-nested-for-loops-in-coffeescript
で議論されているように、

LANG:coffeescript
list = ["a", "b"]

`outer://`
for i in list
    for j in [1..2]
        alert(i + j)
        `continue outer`

バッククオートでエスケープしたラベルや continue を渡してやるのが良いみたいです。

`outer:` ではなく `outer://` となっている理由は、 上記コードを JavaScript に変換して

LANG:javascript
var i, j, list, _i, _j, _len;

list = ["a", "b"];

outer://;

for (_i = 0, _len = list.length; _i < _len; _i++) {
  i = list[_i];
  for (j = _j = 1; _j <= 2; j = ++_j) {
    alert(i + j);
    continue outer;
  }
}

となるのを見ると明らかだと思います。

outer: の後ろに ; が入るので、それをコメントアウトしているわけですね。

書いてみた

日本の休日を求めるスクリプト(休日判定)

まあ、車輪の再発明っぽいところもあるわけですが、 祝日名が取れて、なおかつメンテナンスしやすい形の ライブラリが見付からなかったので書いてみました。

一応、https://gist.github.com/Songmu/703311 と突き合わせて 齟齬がないことを確認しましたが、運用は個々の責任でお願いします。

法律改正などで休日が変更になっても、よほどのことがない限り definition のところをいじるだけで対応できるはず???

最新版はこちらです

現在はこちらで公開しています。

ソフトウェア/javascript/japanese-holidays

ソース

以下のコードは古いです

最新版はこちらで公開しています。

ソフトウェア/javascript/japanese-holidays

LANG:coffeescript(linenumber)
###

    日本の休日を JavaScript で計算するためのライブラリ
                         Osamu Takeuchi <osamu@big.jp>

    ChangeLog
        2013.04.17 初出

    Date クラスに以下の関数を追加する

    **** Date::isHoliday(furikae = true)

    指定された日が休日かどうかを判定して、休日なら名前を返す
    休日でなければ null を返す
    furikae に false を指定すると振替休日を除く
    内部ではキャッシュした値を使って計算するため繰り返し呼ぶ
    際にはとても高速に動作する
    
    JavaScript:

      today = new Date();
      holiday = today.isHoliday();
      if(holiday) {
          alert("今日は " + holiday + " です<br/>");
      } else {
          alert("今日は祝日ではありません<br/>");
      }


    **** Date.getHolidaysOf(year, furikae = true)
    
    指定された年の休日を配列にして返す
    配列には {month:m, date:d, name:s} の形で表わされた休日が日付順に並ぶ
    furikae に false を指定すると、振替休日および国民の休日を除く

    JavaScript:
    
    today = new Date();
    holidays = Date.getHolidaysOf( today.getFullYear() );
    for(holiday in holidays) {
        document.write(
            holiday.month + "月" + holiday.date + "日は " +
            holiday.name + " です<br/>"
        );
    }


   **** Date::getShifted(year, mon, day, hour, min, sec, msec )

    元の時刻から指定時間だけずらした時刻を生成して返す
    負の数も指定できる

    d = new Date();
    d.getShifted(1);        # 1年後の時刻
    d.getShifted(0, -10);   # 10ヶ月前の時刻
    d.getShifted(0,0,0,1);  # 1時間後の時刻

###

Date::getShifted = (year, mon, day, hour, min, sec, msec) ->
    # まずは日付以下の部分を msec に直して処理する
    res = new Date()
    res.setTime( @getTime() + 
        (((( day ? 0 ) * 24 + ( hour ? 0 )) * 60 + ( min ? 0 )) * 60 + 
                                ( sec ? 0 )) * 1000 + ( msec ? 0 )
    )
    # 年と月はちょっと面倒な処理になる
    res.setFullYear res.getFullYear() + ( year ? 0 ) + 
       Math.floor( ( res.getMonth() + ( mon ? 0 ) ) / 12 )
    res.setMonth ( ( res.getMonth() + ( mon ? 0 ) ) % 12 + 12 ) % 12
    return res

###
    ヘルパ関数
###

# 年を与えると指定の祝日を返す関数を作成
simpleHoliday = (month, day) ->
    (year) -> new Date(year, month-1, day)


# 年を与えると指定の月の nth 月曜を返す関数を作成
happyMonday = (month, nth) ->
    (year) ->
        monday = 1
        first = new Date(year, month-1, 1)
        first.getShifted( 0, 0,
            ( 7 - ( first.getDay() - monday ) ) % 7 + ( nth - 1 ) * 7
        )


# 年を与えると春分の日を返す
shunbun = (year) ->
    date = new Date()
    date.setTime( -655910271894.040039 + 31556943676.430065 * (year-1949) + 24*3600*1000/2 )
    new Date(year, date.getMonth(), date.getDate())


# 年を与えると秋分の日を返す
shubun = (year) ->
    date = new Date()
    date.setTime( -671361740118.508301 + 31556929338.445450 * (year-1948) + 24.3*3600*1000/2 )
    new Date(year, date.getMonth(), date.getDate())


###
    休日データ
    https://ja.wikipedia.org/wiki/%E5%9B%BD%E6%B0%91%E3%81%AE%E7%A5%9D%E6%97%A5
###

definition = [
    [ "元旦",               simpleHoliday( 1,  1), 1949       ],
    [ "成人の日",           simpleHoliday( 1, 15), 1949, 1999 ],
    [ "成人の日",           happyMonday(   1,  2), 2000       ],
    [ "建国記念の日",       simpleHoliday( 2, 11), 1967       ],
    [ "昭和天皇の大喪の礼", simpleHoliday( 2, 24), 1989, 1989 ],
    [ "春分の日",           shunbun,               1949       ],
    [ "明仁親王の結婚の儀", simpleHoliday( 4, 10), 1959, 1959 ],
    [ "天皇誕生日",         simpleHoliday( 4, 29), 1949, 1988 ],
    [ "みどりの日",         simpleHoliday( 4, 29), 1989, 2006 ],
    [ "昭和の日",           simpleHoliday( 4, 29), 2007       ],
    [ "憲法記念日",         simpleHoliday( 5,  3), 1949       ],
    [ "みどりの日",         simpleHoliday( 5,  4), 2007       ],
    [ "こどもの日",         simpleHoliday( 5,  5), 1949       ],
    [ "徳仁親王の結婚の儀", simpleHoliday( 6,  9), 1993, 1993 ],
    [ "海の日",             simpleHoliday( 7, 20), 1996, 2002 ],
    [ "海の日",             happyMonday(   7,  3), 2003       ],
    [ "敬老の日",           simpleHoliday( 9, 15), 1966, 2002 ],
    [ "敬老の日",           happyMonday(   9,  3), 2003       ],
    [ "秋分の日",           shubun,                1948       ],
    [ "体育の日",           simpleHoliday(10, 10), 1966, 1999 ],
    [ "体育の日",           happyMonday(  10,  2), 2000       ],
    [ "文化の日",           simpleHoliday(11,  3), 1948       ],
    [ "即位の礼正殿の儀",   simpleHoliday(11, 12), 1990, 1990 ],
    [ "勤労感謝の日",       simpleHoliday(11, 23), 1948       ],
    [ "天皇誕生日",         simpleHoliday(12, 23), 1989       ],
]


# 休日を与えるとその振替休日を返す
# 振り替え休日がなければ null を返す
furikaeHoliday = (holiday) ->
    # 振替休日制度制定前 または 日曜日でない場合 振り替え無し
    sunday = 0
    if holiday < new Date(1973, 4-1, 30-1) or holiday.getDay() != sunday
        return null
    # 日曜日なので一日ずらす
    furikae = holiday.getShifted(0, 0, 1)
    # ずらした月曜日が休日でなければ振替休日
    if !furikae.isHoliday(false)
        return furikae
    # 旧振り替え制度では1日以上ずらさない
    if holiday < new Date(2007, 1-1,  1)
        return null # たぶんこれに該当する日はないはず?
    loop
        # 振り替えた結果が休日だったら1日ずつずらす
        furikae = furikae.getShifted(0, 0, 1)
        if !furikae.isHoliday(false)
            return furikae


# 休日を与えると、翌日が国民の休日かどうかを判定して、
# 国民の休日であればその日を返す
kokuminHoliday = (holiday) ->
    if holiday.getFullYear() < 1988 # 制定前
        return null
    # 2日後が振り替え以外の祝日か
    if !holiday.getShifted(0, 0, 2).isHoliday(false)
        return null
    sunday = 0
    monday = 1
    kokumin = holiday.getShifted(0, 0, 1)
    if kokumin.isHoliday(false) or  # 次の日が祝日
       kokumin.getDay()==sunday or  # 次の日が日曜
       kokumin.getDay()==monday     # 次の日が月曜(振替休日になる)
        return null
    return kokumin


#
# holidays[furikae] = {
#    1999:
#      "1,1": "元旦"
#      "1,15": "成人の日"
#      ...
# }
#
holidays = { true: {}, false: {} }

getHolidaysOf = (y, furikae) ->
    # キャッシュされていればそれを返す
    furikae = if !furikae? or furikae then true else false
    cache = holidays[furikae][y]
    return cache if cache?
    # されてなければ計算してキャッシュ
    # 振替休日を計算するには振替休日以外の休日が計算されて
    # いないとダメなので、先に計算する
    wo_furikae = {}
    for entry in definition
        continue if entry[2]? && y < entry[2]   # 制定年以前
        continue if entry[3]? && entry[3] < y   # 廃止年以降
        holiday = entry[1](y)                   # 休日を計算
        continue unless holiday?                # 無効であれば無視
        m = holiday.getMonth()+1                # 結果を登録
        d = holiday.getDate()
        wo_furikae[ [m,d] ] = entry[0]
    holidays[false][y] = wo_furikae
    
    # 国民の休日を追加する
    kokuminHolidays = []
    for month_day of wo_furikae
        month_day = month_day.split(",")
        holiday = kokuminHoliday( new Date(y, month_day[0]-1, month_day[1] ) )
        if holiday?
            m = holiday.getMonth()+1            # 結果を登録
            d = holiday.getDate()
            kokuminHolidays.push([m,d])
    for holiday in kokuminHolidays
        wo_furikae[holiday] = "国民の休日"
    
    # 振替休日を追加する
    w_furikae = {}
    for month_day, name of wo_furikae
        w_furikae[month_day] = name
        month_day = month_day.split(",")
        holiday = furikaeHoliday( new Date(y, month_day[0]-1, month_day[1] ) )
        if holiday?
            m = holiday.getMonth()+1            # 結果を登録
            d = holiday.getDate()
            w_furikae[ [m,d] ] = "振替休日"
    holidays[true][y] = w_furikae               # 結果を登録
    return holidays[furikae][y]

Date.getHolidaysOf = (y, furikae) ->
    # データを整形する
    result = []
    for month_day, name of getHolidaysOf(y, furikae)
        result.push(
            month : parseInt(month_day.split(",")[0])
            date  : parseInt(month_day.split(",")[1])
            name  : name
        )
    # 日付順に並べ直す
    result.sort( (a,b)-> (a.month-b.month) or (a.date-b.date) )
    result

Date::isHoliday = (furikae) ->
    return getHolidaysOf(@getFullYear(), furikae)[ [@getMonth()+1, @getDate()] ]

JavaScript に直して minify したもの

LANG:javascript_dom
(function(){var definition;var furikaeHoliday;var getHolidaysOf;var happyMonday;var holidays;var kokuminHoliday;var shubun;var shunbun;var simpleHoliday;Date.prototype.getShifted=function(year,mon,day,hour,min,sec,msec){var res;res=new Date;res.setTime(this.getTime()+((((day!=null?day:0)*24+(hour!=null?hour:0))*60+(min!=null?min:0))*60+(sec!=null?sec:0))*1E3+(msec!=null?msec:0));res.setFullYear(res.getFullYear()+(year!=null?year:0)+Math.floor((res.getMonth()+(mon!=null?mon:0))/12));res.setMonth(((res.getMonth()+
(mon!=null?mon:0))%12+12)%12);return res};simpleHoliday=function(month,day){return function(year){return new Date(year,month-1,day)}};happyMonday=function(month,nth){return function(year){var first;var monday;monday=1;first=new Date(year,month-1,1);return first.getShifted(0,0,(7-(first.getDay()-monday))%7+(nth-1)*7)}};shunbun=function(year){var date;date=new Date;date.setTime(-6.5591027189404E11+3.1556943676430065E10*(year-1949)+24*3600*1E3/2);return new Date(year,date.getMonth(),date.getDate())};
shubun=function(year){var date;date=new Date;date.setTime(-6.713617401185083E11+3.155692933844545E10*(year-1948)+24.3*3600*1E3/2);return new Date(year,date.getMonth(),date.getDate())};definition=[["元旦",simpleHoliday(1,1),1949],["成人の日",simpleHoliday(1,15),1949,1999],["成人の日",happyMonday(1,2),2000],["建国記念の日",simpleHoliday(2,11),1967],["昭和天皇の大喪の礼",simpleHoliday(2,24),1989,1989],["春分の日",shunbun,1949],["明仁親王の結婚の儀",simpleHoliday(4,10),1959,1959],["天皇誕生日",simpleHoliday(4,29),1949,1988],
["みどりの日",simpleHoliday(4,29),1989,2006],["昭和の日",simpleHoliday(4,29),2007],["憲法記念日",simpleHoliday(5,3),1949],["みどりの日",simpleHoliday(5,4),2007],["こどもの日",simpleHoliday(5,5),1949],["徳仁親王の結婚の儀",simpleHoliday(6,9),1993,1993],["海の日",simpleHoliday(7,20),1996,2002],["海の日",happyMonday(7,3),2003],["敬老の日",simpleHoliday(9,15),1966,2002],["敬老の日",happyMonday(9,3),2003],["秋分の日",shubun,1948],["体育の日",simpleHoliday(10,10),1966,1999], ["体育の日",happyMonday(10,2),2000],["文化の日",simpleHoliday(11,3),
1948],["即位の礼正殿の儀",simpleHoliday(11,12),1990,1990],["勤労感謝の日",simpleHoliday(11,23),1948],["天皇誕生日",simpleHoliday(12,23),1989],];furikaeHoliday=function(holiday){var furikae;var sunday;var _results;sunday=0;if(holiday<new Date(1973,4-1,30-1)||holiday.getDay()!==sunday)return null;furikae=holiday.getShifted(0,0,1);if(!furikae.isHoliday(false))return furikae;if(holiday<new Date(2007,1-1,1))return null;for(_results=[];true;){furikae=furikae.getShifted(0,0,1);if(!furikae.isHoliday(false))return furikae}return _results};
kokuminHoliday=function(holiday){var kokumin;var monday;var sunday;if(holiday.getFullYear()<1988)return null;if(!holiday.getShifted(0,0,2).isHoliday(false))return null;sunday=0;monday=1;kokumin=holiday.getShifted(0,0,1);if(kokumin.isHoliday(false)||kokumin.getDay()===sunday||kokumin.getDay()===monday)return null;return kokumin};holidays={"true":{},"false":{}};getHolidaysOf=function(y,furikae){var cache;var d;var entry;var holiday;var kokuminHolidays;var m;var month_day;var name;var w_furikae;var wo_furikae;
var _i;var _j;var _len;var _len2;furikae=!(furikae!=null)||furikae?true:false;cache=holidays[furikae][y];if(cache!=null)return cache;wo_furikae={};for(_i=0,_len=definition.length;_i<_len;_i++){entry=definition[_i];if(entry[2]!=null&&y<entry[2])continue;if(entry[3]!=null&&entry[3]<y)continue;holiday=entry[1](y);if(holiday==null)continue;m=holiday.getMonth()+1;d=holiday.getDate();wo_furikae[[m,d]]=entry[0]}holidays[false][y]=wo_furikae;kokuminHolidays=[];for(month_day in wo_furikae){month_day=month_day.split(",");
holiday=kokuminHoliday(new Date(y,month_day[0]-1,month_day[1]));if(holiday!=null){m=holiday.getMonth()+1;d=holiday.getDate();kokuminHolidays.push([m,d])}}for(_j=0,_len2=kokuminHolidays.length;_j<_len2;_j++){holiday=kokuminHolidays[_j];wo_furikae[holiday]="国民の休日"}w_furikae={};for(month_day in wo_furikae){name=wo_furikae[month_day];w_furikae[month_day]=name;month_day=month_day.split(",");holiday=furikaeHoliday(new Date(y,month_day[0]-1,month_day[1]));if(holiday!=null){m=holiday.getMonth()+
1;d=holiday.getDate();w_furikae[[m,d]]="振替休日"}}holidays[true][y]=w_furikae;return holidays[furikae][y]};Date.getHolidaysOf=function(y,furikae){var month_day;var name;var result;var _ref;result=[];_ref=getHolidaysOf(y,furikae);for(month_day in _ref){name=_ref[month_day];result.push({month:parseInt(month_day.split(",")[0]),date:parseInt(month_day.split(",")[1]),name:name})}result.sort(function(a,b){return a.month-b.month||a.date-b.date});return result};Date.prototype.isHoliday=function(furikae){return getHolidaysOf(this.getFullYear(),
furikae)[[this.getMonth()+1,this.getDate()]]}}).call(this);

コメントやスペースをすべて除いたタイプ量を比べると

Coffee Script3650 文字
Java Script4751 文字
比率0.768

となって、この単純な例では Coffee Script を使うことによって タイプ量が 3/4 程度で済んでいることになります。

これを使ってカレンダーを表示

calendar.png

休日セルにマウスを重ねると休日名がポップアップします。

使うときは、

LANG:coffeescript
target = document.getElementById("target_id");
target.appendChild( createCalendar(2013, 3) );
target.appendChild( createCalendar(2013, 4) );
target.appendChild( createCalendar(2013, 5) );

などとします。

LANG:coffeescript(linenumber)
createElement = (tag, parent, innerHTML) ->
    child = document.createElement(tag)
    parent.appendChild(child) if parent?
    child.innerHTML = innerHTML if innerHTML?
    return child

createCalendar = (year, month) ->
    calendar = createElement("div");
    calendar.className = "calendar"
    title = createElement("div", calendar);
    title.className = "title"
    a = createElement("a", title, year+"/"+month)
    a.name = "#"+year+"/"+month
    tab = createElement("table", calendar);
    d = new Date(year+"/"+month+"/1")
    i = 0
    while(i < d.getDay())
        tr = createElement("tr", tab) if (i==0)
        createElement("td", tr).className = "empty"
        i++
    while(month == d.getMonth() + 1) 
        tr = createElement("tr", tab) if (d.getDay()==0)
        td = createElement("td", tr, d.getDate())
        holiday = d.isHoliday()
        if holiday
            td.className = "holiday"
            td.setAttribute("holiday", holiday)
        d = d.getShifted(0,0,1)
    return calendar

土曜日と日曜日は css の nth-child で色を付ければ Script 側で特別処理をしておく必要はありません。

LANG:css
div.calendar {
    float: left;
    padding: 5px;
}

div.calendar td {
    font-family: Arial, Helvetica, Sans-serif;
}

div.calendar div.title {
    text-align: center;
    font-size: large;
    font-weight: bold;
    background: #666;
    color: #eee;
}

div.calendar table {
    margin:auto;
}

div.calendar table tr td {
    text-align: center;
    color: grey;
}

div.calendar table tr td:nth-child(7):not(.empty):not(.holiday) {
    background: #cce;
}

div.calendar table tr td.holiday,
div.calendar table tr td:first-child:not(.empty) {
    background: #ecc;
}

div.calendar table tr td.holiday:hover:after {
    content: attr(holiday);
    position: absolute;
    margin-top: -2em;
    background: #ecc;
    padding: 2px;
    box-shadow: 1px 1px 3px grey;
    -moz-box-shadow: 1px 1px 3px grey;
    -webkit-box-shadow: 1px 1px 3px grey;
}

Array::first(test=->true), Array::last(test=->true)

引数無しなら最初と最後を返すが、
引数に (element)->Boolean を渡すと、テストが成功する物のうち、 始めと最後を返すので、検索に使える。

見付からなければ null を返す。

ソース

LANG:coffeescript
# test を満たす最後の要素を返す
Array::last = ( test = (e)->true ) ->
    return null if @length==0
    for i in [@length-1 .. 0]
        if test(@[i])
            return @[i]
    return null

# test を満たす初めての要素を返す
Array::first = ( test = (e)->true ) ->
    for elem in this
        if test(elem)
            return elem
    return null

Object.is_a, Object::is_a

javascript の instanceof 演算子が壊れた仕様であることは有名ですが、

参照: http://minghai.github.io/library/coffeescript/07_the_bad_parts.html
    の typeof の項

調べたいオブジェクトを Object() で包んでから渡してやりさえすれば、 ほぼ期待通りの動作をしてくれるのだそうです。

参照: http://webreflection.blogspot.jp/2011/10/better-isa-function-for-js.html

で、is_a という関数を定義する際、 obj.is_a(type) と書けるようにすればタイプ量が少なくて一見良さそうなのですが、 null や undefined に対して呼べなくなってしまうため、 javascript から使うには使いにくくなってしまいます。 そこで、javascript 用には Object.is_a(obj, type) という仕様の関数を用意しました。

一方 Coffee Script では ? を挟んで obj?.is_a(type) と呼べば null や undefined にも対応できるので、 Object::is_a(type) も便利に使えます。さらに、obj.is_a(type) 形式で呼び出す場合には、 is_a の呼び出し時に数値や文字列などのネイティブオブジェクトが Object にカプセル化されるため、 is_a 関数内でもう一度カプセル化してやる必要はないようです。

ただ、異なるフレームで作成されたオブジェクトに対してこの関数を呼んでも正しく判定できないようです。
参照:http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/

ソース

LANG:coffeescript
Object::is_a = (type)->
  this instanceof type

Object.is_a = (obj, type)->
  obj? and Object(obj) instanceof type

テスト

LANG:coffeescript
# for jasmine
describe "Object.is_a", ->

  it "distinguishes Number", ->
    expect( Object.is_a(1, Number) ).toBe true
    expect( Object.is_a(1, Object) ).toBe true
    expect( Object.is_a(new Number(1), Number) ).toBe true
    
  it "distinguishes String", ->
    expect( Object.is_a("a", String) ).toBe true
    expect( Object.is_a("a", Object) ).toBe true
    expect( Object.is_a(new String("a"), String) ).toBe true
  
  it "distinguishes RegExp", ->
    expect( Object.is_a(/a/, RegExp) ).toBe true
    expect( Object.is_a(/a/, Object) ).toBe true
    expect( Object.is_a(new RegExp("a"), RegExp) ).toBe true
  
  it "distinguishes Boolean", ->
    expect( Object.is_a(true, Boolean) ).toBe true
    expect( Object.is_a(true, Object) ).toBe true
    expect( Object.is_a(1==1, Boolean) ).toBe true
    expect( Object.is_a(new Boolean(true), Boolean) ).toBe true
  
  it "distinguishes Array", ->
    expect( Object.is_a([], Array) ).toBe true
    expect( Object.is_a([], Object) ).toBe true
    expect( Object.is_a(new Array([]), Array) ).toBe true

  it "distinguishes Object", ->
    expect( Object.is_a({}, Object) ).toBe true
  
  it "answers false for null", ->
    expect( Object.is_a(null, Object) ).toBe false
  
  it "answers false for undefined", ->
    expect( Object.is_a(undefined, Object) ).toBe false

  it "distinguishes with ancestors", ->
    class A
    class B extends A
    class C extends B
    expect( Object.is_a(new C, Object) ).toBe true
    expect( Object.is_a(new C, A) ).toBe true
    expect( Object.is_a(new C, B) ).toBe true
    expect( Object.is_a(new C, C) ).toBe true
    expect( Object.is_a(new B, Object) ).toBe true
    expect( Object.is_a(new B, A) ).toBe true
    expect( Object.is_a(new B, B) ).toBe true
    expect( Object.is_a(new B, C) ).toBe false
    expect( Object.is_a(new A, Object) ).toBe true
    expect( Object.is_a(new A, A) ).toBe true
    expect( Object.is_a(new A, B) ).toBe false
    expect( Object.is_a(new A, C) ).toBe false

  it "distinguishes objects with constructor of object", ->
    class A
    class B extends A
    o = {}
    a = new A
    b = new B

    expect( o.constructor ).toBe Object
    expect( a.constructor ).toBe A
    expect( b.constructor ).toBe B
    expect( o.constructor.__super__ ).toBeUndefined()
    expect( a.constructor.__super__ ).toBeUndefined()
    expect( b.constructor.__super__.constructor ).toBe A
    
    expect( Object.is_a(o, o.constructor) ).toBe true
    expect( Object.is_a(o, a.constructor) ).toBe false
    expect( Object.is_a(o, b.constructor) ).toBe false
    expect( Object.is_a(a, o.constructor) ).toBe true
    expect( Object.is_a(a, a.constructor) ).toBe true
    expect( Object.is_a(a, b.constructor) ).toBe false
    expect( Object.is_a(b, o.constructor) ).toBe true
    expect( Object.is_a(b, a.constructor) ).toBe true
    expect( Object.is_a(b, b.constructor) ).toBe true

 
describe "Object::is_a", ->

  it "works with undefined", ->
    expect( undefined?.is_a(Object) ).toBeFalsy()
    expect( undefined_obj?.is_a(Object) ).toBeFalsy()
    `var uninitialized_obj`
    expect( uninitialized_obj?.is_a(Object) ).toBeFalsy()

  it "works with null", ->
    expect( null?.is_a(Object) ).toBeFalsy()
  
  it "works with literal objects", ->
    expect( 0 instanceof Number      ).toBeFalsy()
    expect( NaN instanceof Number    ).toBeFalsy()
    expect( 0?.is_a(Number)          ).toBeTruthy()
    expect( NaN?.is_a(Number)        ).toBeTruthy()
    expect( "a" instanceof String    ).toBeFalsy()
    expect( "a"?.is_a(String)        ).toBeTruthy()
    expect( /a/ instanceof RegExp    ).toBeTruthy()
    expect( /a/?.is_a(RegExp)        ).toBeTruthy()
    expect( false instanceof Boolean ).toBeFalsy()
    expect( (0==0) instanceof Boolean).toBeFalsy()
    expect( false?.is_a(Boolean)     ).toBeTruthy()
    expect( (0==0)?.is_a(Boolean)    ).toBeTruthy()
    expect( [] instanceof Array      ).toBeTruthy()
    expect( []?.is_a(Array)          ).toBeTruthy()
    expect( {} instanceof Object     ).toBeTruthy()
    expect( {}?.is_a(Object)         ).toBeTruthy()

  it "works with the super class", ->
    class A
    class B extends A
    class C extends B
    a = new A
    b = new B
    c = new C
    expect( c?.is_a(Object) ).toBeTruthy()
    expect( c?.is_a(A) ).toBeTruthy()
    expect( c?.is_a(B) ).toBeTruthy()
    expect( c?.is_a(C) ).toBeTruthy()
    expect( b?.is_a(Object) ).toBeTruthy()
    expect( b?.is_a(A) ).toBeTruthy()
    expect( b?.is_a(B) ).toBeTruthy()
    expect( b?.is_a(C) ).toBeFalsy()
    expect( a?.is_a(Object) ).toBeTruthy()
    expect( a?.is_a(A) ).toBeTruthy()
    expect( a?.is_a(B) ).toBeFalsy()
    expect( a?.is_a(C) ).toBeFalsy()

Object.class_name_of

こちらも null や undefined に対応するため、Object.class_name_of(obj) の形になっています。

http://webreflection.blogspot.jp/2007/01/javascript-getclass-and-isa-functions.html のコードはたぶん意図と異なるコードになっているので、気持ちを汲み取って書き直してみました。

http://api.jquery.com/jQuery.type/
http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/

このあたりを見ると Function.prototype.toString() は実装依存なため下記コードは 必ずしもすべてのブラウザで動かない可能性もあるのですが、とりあえず試した範囲では大丈夫そう、 という感じです。

ソース

LANG:coffeescript
root = exports ? this

Object.class_name_of = (obj)->
  return "null" if obj == null
  return "undefined" if obj == undefined
  if obj.constructor.toString().match /function\s+(\S+)\s*\(/
    return RegExp.$1
  if obj.constructor.prototype?
    # search constructor in the global scope
    for key, value of root
      if value?.prototype? and
         obj instanceof value
        return key
  return "anonymous"

単純版のソース

上記のコードがややこしい理由は、

LANG:javascript
function MyClass(){};
obj = new MyClass();
alert(Object.class_name_of(obj));

ではなく、

LANG:javascript
MyClass = function(){};  // グローバルに宣言された変数
obj = new MyClass();
alert(Object.class_name_of(obj));

とした場合にも名前を判別できるよう工夫しているためです。 (たぶん、リンク先ではそのあたりのコードがおかしくなっているように思えるのですが・・・)

その際、グローバルオブジェクトのプロパティをスキャンしているため、 この関数を無名クラスのオブジェクトに対して頻繁に呼び出すと、 パフォーマンス的に問題が生じる可能性があります。

Coffee Script を使っている限り 上記のような無名クラスのオブジェクトを作成することはほとんどありませんし、 そもそも上のように頑張ったところで、

LANG:javascript
var MyClass = function(){};  // ローカルに宣言された変数
obj = new MyClass();
alert(Object.class_name_of(obj));

こういう場合にはうまく行きません。

そこで無名関数から作成されたオブジェクトに対して "anonymous" を返して構わないことにすれば、 if obj.constructor.prototype? の部分を除いた次のコードで良いことになります。

LANG:coffeescript
Object.class_name_of = (obj)->
  return "null" if obj == null
  return "undefined" if obj == undefined
  if obj.constructor.toString().match /function\s+(\S+)\(/
    return RegExp.$1
  return "anonymous"

テスト

LANG:coffeescript
# for jasmine
root = exports ? this

describe "Object.class_name_of", ->

  it "distinguishes null", ->
    expect( Object.class_name_of(null) ).toBe "null"

  it "distinguishes undefined", ->
    expect( Object.class_name_of(undefined) ).toBe "undefined"
    a = {}
    expect( Object.class_name_of(a["abc"]) ).toBe "undefined"

  it "distinguishes Object", ->
    expect( Object.class_name_of({}) ).toBe "Object"
    expect( Object.class_name_of(new Object()) ).toBe "Object"

  it "distinguishes Number", ->
    expect( Object.class_name_of(1) ).toBe "Number"
    expect( Object.class_name_of(new Object(1)) ).toBe "Number"
    expect( Object.class_name_of(Number(1)) ).toBe "Number"
    expect( Object.class_name_of(new Number(1)) ).toBe "Number"

  it "distinguishes Array", ->
    expect( Object.class_name_of([]) ).toBe "Array"
    expect( Object.class_name_of(new Object([])) ).toBe "Array"
    expect( Object.class_name_of(new Array()) ).toBe "Array"

  it "distinguishes RegExp", ->
    expect( Object.class_name_of(/a/) ).toBe "RegExp"
    expect( Object.class_name_of(new Object(/a/)) ).toBe "RegExp"
    expect( Object.class_name_of(new RegExp("a")) ).toBe "RegExp"

  it "distinguishes Boolean", ->
    expect( Object.class_name_of(true) ).toBe "Boolean"
    expect( Object.class_name_of(new Object(true)) ).toBe "Boolean"
    expect( Object.class_name_of(1==0) ).toBe "Boolean"
    expect( Object.class_name_of(new Boolean(true)) ).toBe "Boolean"

  it "distinguishes User class", ->
    class A
    expect( Object.class_name_of(A) ).toBe "Function"
    expect( Object.class_name_of(new A) ).toBe "A"
    class B extends A
    expect( Object.class_name_of(B) ).toBe "Function"
    expect( Object.class_name_of(new B) ).toBe "B"

  it "finds constructor stored in a global variable", ->
    root.global_func = ->
    expect( Object.class_name_of( new global_func() ) ).toBe "global_func"
  
#  for simplified version
#
#  it "does not find constructor stored in a global variable", ->
#    root.global_func = ->
#    expect( Object.class_name_of( new global_func() ) ).toBe "anonymous"
   
  it "does not find constructor stored in a local variable", ->
    local_func = ->
    expect( Object.class_name_of( new local_func() ) ).toBe "anonymous"

  it "returns variety of names for DOM objects depending on the environment", ->
    expect( Object.class_name_of( window ) ).toMatch /^(Window|constructor|anonymous)$/
    expect( Object.class_name_of( document ) ).toMatch /^(HTMLDocument|XMLDocument|anonymous)$/
    expect( Object.class_name_of( location ) ).toMatch /^(Location|Object|anonymous)$/

  it "can find super class via __super__ for Coffee Script' classes", ->
    class A
    class B extends A
    a = new A
    b = new B

    cn_of = Object.class_name_of
    expect(cn_of                                   A           ).toBe "Function"
    expect(                                        A.__super__ ).toBeUndefined()
    expect(                            a.constructor           ).toBe A
    expect(cn_of                       a.constructor           ).toBe "Function"
    expect(                            a.constructor.__super__ ).toBeUndefined()
    expect(cn_of                       a                       ).toBe "A"
    expect(cn_of             B                                 ).toBe "Function"
    expect(cn_of             B.__super__                       ).toBe "A"
    expect(                  B.__super__.constructor           ).toBe A
    expect(                  B.__super__.constructor.__super__ ).toBeUndefined()
    expect(      b.constructor                                 ).toBe B
    expect(cn_of b.constructor                                 ).toBe "Function"
    expect(cn_of b.constructor.__super__                       ).toBe "A"
    expect(      b.constructor.__super__.constructor           ).toBe A
    expect(      b.constructor.__super__.constructor.__super__ ).toBeUndefined()

  it "returns only the last part of nested names", ->
    class A
    class A.B
    expect(Object.class_name_of new A.B).toBe "B"

  it "returns dummy name of a named function expression", ->
    # http://bonsaiden.github.io/JavaScript-Garden/#function.general
    C = `function D(){}`
    expect(Object.class_name_of new C).toBe "D"

  it "faces some problems with IE8", ->
    cn_of = Object.class_name_of
    # this is OK
    expect(cn_of new Error()          ).toBe "Error" 
    # IE8 returns "anonymous" for these Error objects
    expect(cn_of new EvalError()      ).toMatch "anonymous|EvalError" 
    expect(cn_of new TypeError()      ).toMatch "anonymous|TypeError"
    expect(cn_of new RangeError()     ).toMatch "anonymous|RangeError"
    expect(cn_of new ReferenceError() ).toMatch "anonymous|ReferenceError"
    expect(cn_of new SyntaxError()    ).toMatch "anonymous|SyntaxError"
    expect(cn_of new URIError()       ).toMatch "anonymous|URIError"

コメント・質問




春分、秋分の計算式について

[じぞう] (2013-11-23 (土) 22:44:27)

初めまして、ネット検索でこのページを見つけました。

題名のとおり祝日判定したくいろいろと探しておりまして
作者様のソースでわからないところがあり質問させていた
だきます。

春分、秋分の計算式(setTime)のところ(それぞれの数値が何なのか)
について解説いただけないでしょうか。

ほとんど解説をお願いしてしまうことになり恐縮ですが
よろしくお願いします。

  • ご存じの通り春分、秋分の日は年によって日付が異なります。これは、1年が正確に365日ではなく、その差を埋めるために閏年が挟まるためです。実のところ、春分、秋分はほぼ正確に等間隔でやってくるのですが、カレンダーの方が閏年分だけ等間隔にならず、そのために日付が異なることになります。

    Wikipedia の春分秋分のページに 1900年〜2099年のデータがあって、そこから春分・秋分の周期を求めると 約31556900000ミリ秒 = 約365.2423 日 でした。この周期を使って春分・秋分を計算する式が上記になります。この式で 少なくともここ200年くらいの日付が再現できていたと思います。
    ただ今見ると、同じページの 2001〜2014年の、時刻まで出ている表を使う方がより正確な式を作れそうですね。100年以上先のカレンダーが必要になることは少なそうなのであまり気にしなくても良いのでしょうけれど。 -- [武内(管理人)]


添付ファイル: filecalendar.png 5893件 [詳細]

Counter: 10211 (from 2010/06/03), today: 1, yesterday: 0