プログラミング/CoffeeScript のバックアップ(No.9)

更新


公開メモ

CoffeeScript

http://coffeescript.org/

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

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 のところをいじるだけで対応できるはず???

ソース

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

コメント・質問





Counter: 10212 (from 2010/06/03), today: 2, yesterday: 0