JavaScriptの迷宮

2013年10月31日 (木)

JavaScriptでひと苦労~日付オブジェクトの大小比較評価 続き

JavaScriptで、日付オブジェクトを大小比較(時間的な前後の比較)する場合、「等しい」かを評価・判定するのに困った件。

日付オブジェクトを比較演算子「==」で比較すると、中身の値(年月日時分秒ミリ秒)を評価するのではなく、同じオブジェクトかどうかで真偽が返ってくる。new で別々に生成したオブジェクトなら別物なので、「==」の結果は偽となる。

そうこうしていたら、「Crystal-Creation」というサイトに有益な情報があった。

「day1.getTime()」とするところを、「+day1」としても同じ結果が返されるそうだ。
どうやら、日付オブジェクトを算術演算すると、暗黙的に中身の値(基準日時との差、ミリ秒)に変換されるらしい。

そこで、時間的に同じかどうかを評価する場合は、
「day1.getTime() == day2.getTime()」や
「day1 >= day2 && day1 <= day2」の他に、
「day1 - day2 == 0」と
「+day1 == +day2」も使える。

…って言うか、一番最後のパターンが一番スッキリする。

また、文字列に変換して評価する、
「day1.toString() == day2.toString()」という方法もあるが、
「等 ==」「不等 !=」のみで、大小(時間的な前後)の評価には使えない。
文字列の大小比較は、文字コードの比較となるため。

それから、「var day3 = day1;」とすると、
「day1 == day3」は、trueとなる。
これは、day3が日付オブジェクトではなくday1の参照(化身、ショートカットのようなもの)で、示す先の実体はday1と同じだかららしい。
「var day4 =  new Date(day1);」だと、値が同じ別の日付オブジェクトになる。

もうひとつ。
日付として扱いたい“今日”を求めて「var today = new Date();」とやっても、もれなく現在時刻が付いてくる。「今日は何の日」とか「今日の予定は」的な処理をしたい場合には、時刻情報は邪魔になる。
時分秒ミリ秒をゼロにする方法を探したら、いくつか見つかった。

■その1 時刻の値を、それぞれ「0」にリセット
var today1 = new Date();
today1.setHours(0);
today1.setMinutes(0);
today1.setSeconds(0);
today1.setMilliseconds(0);

■その2 今日の日付だけを移し替え
var now = new Date();
var today2 = new Date(now.getFullYear(),now.getMonth(),now.getDate());

■その3 その2を一度にまとめて
var today3 = new Date((new Date()).getFullYear(),(new Date()).getMonth(),(new Date()).getDate());

JavaScriptって、もどかしい。なんだかなぁ~。

さらに初心者がハマりがちなのが、月の扱いかた。

■12月が11で、1月が0

何かにつけて、1を足したり引いたりしなくてはならない。しかし、それなりに理由はありそう。それは、月を「名前(文字列)」や「漢数字」で表現する場合、フォーム(select、optionなど)で扱うには便利。
例えば、

var monthName = [
['January','February','March','April','May','June','July','August','September','October','November','December']
,['JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE','JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER']
,['Jan.','Feb.','Mar.','Apr.','May.','Jun.','Jul.','Aug.','Sep.','Oct.','Nov.','Dec.']
,['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
,['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC']
,['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月']
,['睦月(むつき)','如月(きさらぎ)','弥生(やよい)','卯月(うづき)','皐月(さつき)','水無月(みなづき)','文月(ふみづき)','葉月(はづき)','長月(ながつき)','神無月(かんなづき)','霜月(しもつき)','師走(しわす)']
,['睦月','如月','弥生','卯月','皐月','水無月','文月','葉月','長月','神在月','霜月','師走','出雲の場合']
,['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']
,['ガーネット','アメシスト','アクアマリン、ブラッドストーン、サンゴ','ダイヤモンド','エメラルド、ヒスイ','ムーンストーン、真珠','ルビー','ペリドット、サードニクス','サファイア','オパール、ルマリン','トパーズ、シトリン','トルコ石、ラピスラズリ、タンザナイト','出典:http://www.istone.org/birth.html']
];

といった配列を用意しておき
var nameType = 0;
document.write(monthName[nameType][(new Date()).getMonth()]);
みたいに、表記を切り替えて使う事ができる。

つまり、曜日の扱いと同じということ。
var dayName = [
['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']
,['SUNDAY','MONDAY','TUESDAY','WEDNESDAY','THURSDAY','FRIDAY','SATURDAY']
,['Sun.','Mon.','Tue.','Wed.','Thu.','Fri.','Sat.']
,['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
,['SUN','MON','TUE','WED','THU','FRI','SAT']
,'日月火水木金土'
,['市場へ出掛け糸と麻を買って来た','お風呂をたいて','お風呂に入り','友達が来て','送っていった','糸巻きもせず','おしゃべりばかり','テュリャテュリャテュリャテューリャーリャー']
];

お決まりのデータ(定数)を、予め配列にして用意しておく書き方はいろいろある。
3番目は、ひとつの文字列をカンマで分割して配列に変換するという方法(splitメソッド)で、クォーテーションまたはダブルクォーテーションの数を減らせる。
4番目は、ひとつの文字列は、1文字ずつの配列として扱えるのでそのまま。
var strDayName = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
var strDayName = new Array('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
var strDayName = 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(',');
var strDayName = '日月火水木金土';

ついでに、日付や時間関連のネタいろいろ。

■うるう年(閏年)の判定

グレゴリオ暦で、うるう年か平年(ふつうの年)かを判定する方法は、国立天文台のサイトに書いてある。
アルゴリズムにすると、

  1. もしも、西暦年号が4で割り切れるなら次の例外の判断へ。そうでなければ平年。
  2. もしも、西暦年号が100で割り切れて400で割り切れないなら平年。そうでなければうるう年。

になる。しかし、これを律儀に処理しなくても、javascriptのDateオブジェクトを使うと、簡単に判定できる。
Dateオブジェクトのインスタンスを作り出す(インスタンス化する)コンストラクタ「new Date()」は、存在しない日付を指定しても正しい日付のインスタンスが出来上がることを利用するもの。
例えば、30日までしかない月で31日を指定すると、翌月の1日としてインスタンス化される。
そこで、調べたい年の2月29日で作成したインスタンスの「月」を調べてみて、2月のままなら「うるう年」で、3月になっていたら「平年」だと判る。
ここで、注意しなくてはならないのは、Dateオブジェクトでの2月は1として扱うこと。

例えば、変数「shirabetaitoshi」に調べたい年を入力しておくと、
((new Date(shirabetaitoshi,1,29)).getMonth() == 1)
は、うるう年なら「true」、平年なら「false」になる。

関数にまとめると、こんな感じ。

function isLeapYear(shirabetaitoshi) {
   return ((new Date(shirabetaitoshi,1,29)).getMonth() == 1);
}

■日にちの選択部品(セレクトボックス、ドロップダウンリスト)

日にちを選択するセレクトボックス(SELECT要素)の選択項目(OPTION要素)を、年月に合わせてJavaScriptで自動生成(~28日、~29日、~30日、~31日)する方法。
うるう年(閏年)の判定でつかった、Dateオブジェクトのコンストラクタ「new Date()」の特性を使えば、年や月は関係なく同じアルゴリズムで済む。
Dateオブジェクトを生成した後で月を調べてみて、月が変わっていなければ選択項目(OPTION要素)を選択部品(SELECT要素)に追加する。翌月になっていたら終了する。
Dateオブジェクトを使うと、もうひとつメリットがある。「曜日」も判るため、選択項目(OPTION要素)に曜日つきで日にちを表示できる。もちろん利用者にとっては、とのほうが親切。

■年齢計算

ネットで見つかる年齢を計算するアルゴリズムには、生年月日を年4桁と月2桁と日2桁の計8桁の整数で表して、計算したい日付(たとえば今日)との差(引き算)を求めてから、1万で割って小数点以下を切り捨てるというものがある。
ただ、具体的にスクリプトにするには、さまざまな書き方がある。そのポリシーは、

  • 他の人が読んでも理解しやすい(特に初心者に)
  • コードの量(文字数や行数)が少ない
  • 処理が速くなる
  • 使用するメモリが少なくて済む
  • どのブラウザの、どのバージョンで動くかを決めて対応する

などがあり、それ次第で変わってくる。他にも「コードの美しさ」というのもあるらしい。
初心者なりにスクリプトを書くと、こんな感じ。生年月日の「年」「月」「日」を、変数「y」「m」「d」で渡す。

function getAge1A(y,m,d) {
  var birthday = y*10000 + m*100 + d;
  var now = new Date();
  var today = now.getFullYear()*10000+(now.getMonth()+1)*100+now.getDate();
  return Math.floor((today-birthday)/10000);
}

行数を減らすと、こんな感じ。小数点以下の切り捨て(整数化)は、ビット演算「ビットごとの NOT」2回で済ませている。演算子は「~(チルダとかティルダと読む)」

function getAge1B(y,m,d) {
  return ~~((((new Date()).getFullYear()-y)*10000+((new Date()).getMonth()+1-m)*100+(new Date()).getDate()-d)/10000);
}

生年月日を表す文字列で渡す場合はこんな感じ。区切りの文字は、「-」「/」「.」の3パターンに対応する。但し、「月」「日」は、ふた桁で書かなくてはならない。

function getAge1C(birthday) {
  var today = (new Date()).getFullYear()*10000+((new Date()).getMonth()+1)*100+(new Date()).getDate();
  return ~~((today-birthday.replace(/-|\/|\./g,''))/10000);
}
使い方は、こんな感じ。
document.write(getAge1C('2013/10/31'));

Dateオブジェクトだけで計算するアルゴリズムを考えてみる。まず、「今年」と「生まれ年」の差を求める。次に、“今年の”誕生日のDateオブジェクト(インスタンス)を作り、今日より前か後かを判定する。今日より後なら、まだ誕生日が来ていないので差から1を引いた値が年齢。そうでなければ、既に誕生日を迎えているので、差の値がそのまま年齢。
初心者なりにスクリプトを書くと、こんな感じ。生年月日の「年」「月」「日」を、変数「y」「m」「d」で渡す。


function getAge2A(y,m,d) {
  var now = new Date();
  var age = now.getFullYear() - y;
  var birthday = new Date(now.getFullYear(),m-1,d);
  return (now < birthday)? --age:age;
}

行数を減らすと、こんな感じ。

function getAge2B(y,m,d) {
  return (new Date() < new Date((new Date()).getFullYear(),m-1,d))? ((new Date()).getFullYear() - y - 1):((new Date()).getFullYear() - y);
}

生年月日を表す文字列で渡す場合はこんな感じ。区切りの文字は、「-」「/」「.」の3パターンに対応する。また、「月」「日」は、ひと桁でも問題ない。

function getAge2C(birthdayStr) {
  var now = new Date();
  var birthday = new Date(birthdayStr.replace(/-|\./g,'/'));
  var age = now.getFullYear() - birthday.getFullYear();
  birthday.setFullYear(now.getFullYear());
  return (now < birthday)? --age:age;
}

行数を減らすと、こんな感じ。

function getAge2D(birthdayStr) {
  var birthday = new Date(birthdayStr.replace(/-|\./g,'/'));
  var age = (new Date()).getFullYear() - birthday.getFullYear();
  return (new Date() < birthday.setFullYear((new Date()).getFullYear()))? --age:age;
}

Dateオブジェクトだけで計算すると、「○歳と△日」というように年齢だけでなく日数も計算できる。

function getAge2E(birthdayStr) {
  var birthday = new Date(birthdayStr.replace(/-|\./g,'/'));
  var now = new Date();
  var baseDate = new Date(now.getFullYear(),now.getMonth(),now.getDate());
  var age = baseDate.getFullYear() - birthday.getFullYear();
  birthday.setFullYear(baseDate.getFullYear());
  if(baseDate < birthday) {
    age--;
    birthday.setFullYear(baseDate.getFullYear()-1);
  }
  return [age,(baseDate - birthday)/86400000];
}

使い方は、こんな感じ。

var tanjyoubi = '2013/10/31';
var age = getAge2E(tanjyoubi);
document.write(tanjyoubi + '生まれの人は、' + age[0] + '歳と' + age[1] + '日だ。');

実際には、これだけでは不完全。

  • 引数として渡された値が不適切だった場合の処理
  • うるう年の2月29日生まれの場合の処理
  • 計算した年齢がマイナスになってしまった場合の処理
  • 今日以外の場合の年齢計算処理

が必要。どこかのライブラリにあると思うけど。

■カウントダウン

「~まで、あと○日」みたいなカウントダウンのアルゴリズムを考えてみる。と言っても単純で、Dateオブジェクトのインスタンスの差を、1日あたりのミリ秒単位の時間で割るだけ。
スクリプトにすると、こんな感じ。今日を基準にするなら第二引数は省略。

function daysCountdown(targetDateStr,baseDateStr) {
  var targetDate = new Date(targetDateStr.replace(/-|\./g,'/'));
  if(baseDateStr) {
    var baseDate = new Date(baseDateStr.replace(/-|\./g,'/'));
  }
  else {
    var now = new Date();
    var baseDate = new Date(now.getFullYear(),now.getMonth(),now.getDate());
  }
  return (targetDate - baseDate)/86400000;
}

日数からメッセージを出力するスクリプトは、こんな感じ。

function countdownMessage(targetDateStr,preMes1,preMes2,justMes,postMes1,postMes2,baseDateStr) {
  var message = '';
  var days = daysCountdown(targetDateStr,baseDateStr);
  if(days > 0 ) {
    message = preMes1 + days + preMes2;
  }
  else if(days == 0) {
    message = justMes;
  }
  else if(days < 0) {
    message = postMes1 + (-days) + postMes2;
  }
  return message;
}

使い方は、こんな感じ。
document.write(countdownMessage('2020/2/20','あと','日だ','当日だ','もう','日過ぎた'));

2013年10月29日 (火)

JavaScriptでひと苦労~日付オブジェクトの大小比較評価

JavaScriptで、日付オブジェクトを大小比較(時間的な前後の比較)する場合、「等しい」「等しくない」が、初心者にとってはひと苦労。オブジェクトの概念を理解していないとダメ。

var day1 = new Date("2013/10/29");
var day2 = new Date("2013/10/29");

document.write("day1 "+day1+" "+day1.getMilliseconds()+"msec<br>");
document.write("day2 "+day2+" "+day1.getMilliseconds()+"msec<br>");

document.write("day1 > day2 ");
if (day1 > day2) {document.write("true<br>")}else{document.write("false<br>")}

document.write("day1 < day2 ");
if (day1 < day2) {document.write("true<br>")}else{document.write("false<br>")}

document.write("day1 >= day2 ");
if (day1 >= day2) {document.write("true<br>")}else{document.write("false<br>")}

document.write("day1 <= day2 ");
if (day1 <= day2) {document.write("true<br>")}else{document.write("false<br>")}

document.write("<br>ここからが初心者がハマるところ<br><br>");

document.write("day1 == day2 ");
if (day1 == day2) {document.write("true<br>")}else{document.write("false<br>")}

document.write("day1 != day2 ");
if (day1 != day2) {document.write("true<br>")}else{document.write("false<br>")}

document.write("<br>オブジェクトの比較で、等しいを真で返すのは次の場合のみ。それは、オブジェクトの中身の値を比較しているわけではないから。<br><br>");

document.write("day1 == day1 ");
if (day1 == day1) {document.write("true<br>")}else{document.write("false<br>")}

document.write("day2 == day2 ");
if (day2 == day2) {document.write("true<br>")}else{document.write("false<br>")}

document.write("<br>そこで、こんな風にするようだ<br><br>");

document.write("day1.getTime() == day2.getTime() ");
if (day1.getTime() == day2.getTime()) {document.write("true<br>")}else{document.write("false<br>")}

document.write("<br>こんな手もある<br><br>");

document.write("day1 >= day2 && day1 <= day2 ");
if (day1 >= day2 && day1 <= day2) {document.write("true<br>")}else{document.write("false<br>")}