Rubyで満年齢、満月齢を(ExcelのDatedif)

年齢の「○歳○ヶ月○日」、つまり「何年何ヶ月と何日がカレンダー通りに経ったか」という計算を考えてみます。

年齢の定義
前日の夜中の12時とのこと。2月29日生まれの人は翌年2月28日24時、つまり3月1日0時に年齢が一つ上がることになる。誕生日は3月1日になるのですね。

閏年の定義
通常使われている西暦はグレゴリオ暦であり、
西暦年が4で割り切れる年は閏年
ただし、西暦年が100で割り切れる年は平年
ただし、西暦年が400で割り切れる年は閏年
という規則とのこと

生年月日から満年齢を計算する際、単純に引いて365で割ると閏年分が狂います。

rubyで試してみます

irb
>> require 'date'

例えば1998年3月1日生まれの人は1999年2月28日には

>> (Date.new(1999,2,28) - Date.new(1998,3,1)).to_i
=> 364 #365で割ると1未満で満0年

となり、閏年を挟まない場合は0歳で合いますが、1999年3月1日生まれの人は2000年2月29日には

>> (Date.new(2000,2,29) - Date.new(1999,3,1)).to_i
=> 365 #365で割ると1以上になり満1才と誤ってしまう

となってしまい、2000年3月1日の前日に1才になってしまいます。

普通に考えると閏年を規定する関数が必要ですが、賢い人はいるもので下記のように一旦数値化することにより閏年に無関係に正確に満年齢計算が可能になります。
つまり19990301と20000229という数値にして引き算をするというものです。10000で割ると満年齢が整数値として取得できます。
(2000229-19990301)÷10000=0.9928 → 満0年

>> y = (Date.new(2000,2,29).strftime('%Y%m%d').to_i - Date.new(1999,3,1).strftime('%Y%m%d').to_i)/10000
=> 0 #満0年
>> y = (Date.new(1999,2,28).strftime('%Y%m%d').to_i - Date.new(1998,3,1).strftime('%Y%m%d').to_i)/10000
=> 0 #満0年
>> y = (Date.new(2000,3,1).strftime('%Y%m%d').to_i - Date.new(1999,3,1).strftime('%Y%m%d').to_i)/10000
=> 1 #満1年

小児科領域の場合、1才未満の月齢計算も必要になります。これも満月齢という考え方で良いようです。8月31日生まれの人は10月1日に1ヶ月齢になります。
これも単純に30で割ったりすると実際には1ヶ月は28日〜31日までバラツキがあるのでおかしいことになります。

>> (Date.new(1999,3,1) - Date.new(1999,2,1)).to_i
=> 28 #満1ヶ月だが30で割ると1未満に

しかしこれもstrftime.to_iを使った数値化をよく見ると、万の桁のくり下がりは必ず12、つまり1年=12ヶ月なので月齢は下記のように計算できます。

>> m = (Date.new(1999,3,1).strftime('%m%d').to_i - Date.new(1999,2,1).strftime('%m%d').to_i)/100
=> 1
>> m<0 ? m+12 : m
=> 1 #満1ヶ月

>> m = (Date.new(2000,2,1).strftime('%m%d').to_i - Date.new(1999,11,1).strftime('%m%d').to_i)/100
=> -9
>> m<0 ? m+12 : m
=> 3 #満3ヶ月

>> m = (Date.new(2000,1,31).strftime('%m%d').to_i - Date.new(1999,11,1).strftime('%m%d').to_i)/100
=> -10
>> m<0 ? m+12 : m
=> 2 #満2ヶ月

同じ原理で満年齢、満月齢が分かったところで端数の日数が分かるか、つまり「何年何ヶ月と何日がカレンダー通りに経ったか」という問に答えようとします。

末日の日付が起算日の日付と一緒かより大きい場合は単純に日付を引くだけです。起算日の日付の方が大きい時が問題になります。起算日より満年月を足した年月日を末日より引く操作が必要になります。

先ず満年月をきちんと足せるかどうかを確認

>> (Date.new(1999,1,31) >> 1).to_s #満1ヶ月後
=> "1999-02-28"
>> (Date.new(1999,1,31) >> 1+12*1).to_s #満1年1ヶ月後
=> "2000-02-29"
>> (Date.new(2000,2,29) >> 12*1).to_s #満1年後
=> "2001-02-28"

と大丈夫そう。

>> y = (Date.new(2004,3,12).strftime('%Y%m%d').to_i - Date.new(2000,10,20).strftime('%Y%m%d').to_i)/10000
=> 3 #満3年
>> m = (Date.new(2004,3,12).strftime('%m%d').to_i - Date.new(2000,10,20).strftime('%m%d').to_i)/100
=> -8
>> m = m<0 ? m+12 : m
=> 4 #満2ヶ月
>> d = Date.new(2004,3,12).day - Date.new(2000,10,20).day
=> -10
>> d = (d<0 ? Date.new(2004,3,12) - (Date.new(2000,10,20) >> y*12+m) : d).to_i
=> 21 #端数の日数は21日

メソッド(関数)を書いてみました。

datedif.rb

require 'date'
#
#= DATEDIF function in Excel
#
#Authors::   Toshiki I. Saito
#Version::   1.0 2011-09-08 tosh
#License::   The MIT License
#--
# starting: Starting date
# ending: Ending date
# Interval: Meaning; Description
# y: Years; Complete calendar years between the dates.
# m: Months; Complete calendar months between the dates.
# d: Days; Number of days between the dates.
# ym: Months Excluding Years; Complete calendar months 
#     between the dates as if they were of the same year.
# yd: Days Excluding Years; Complete calendar days 
#     between the dates as if they were of the same year.
# md: Days Excluding Years And Months; Complete calendar days 
#     between the dates as if they were of the same month and same year.
#++
def datedif(starting, ending, interval)
  if ending < starting then false
  else
    interval = interval.downcase
    years = (ending.strftime('%Y%m%d').to_i - starting.strftime('%Y%m%d').to_i)/10000.to_i
    months = (ending.strftime('%m%d').to_i - starting.strftime('%m%d').to_i)/100.to_i
    months = months<0 ? months+12 : months
    days = ending.day - starting.day
    case interval
      when 'y' then years
      when 'm' then years*12 + months
      when 'd' then (ending - starting).to_i
      when 'ym' then months
      when 'yd' then (ending - (starting >> 12*years)).to_i
      when 'md' then (days<0 ? ending - (starting >> 12*years+months) : days).to_i
      else false
    end
  end
end
>> require 'datedif'
=> true
>> today = Date.today
=> #
>> bd = Date.new(1969,3,13)
=> #
>> print 'Your age: '+datedif(bd,today,'y').to_s+' years '+datedif(bd,today,'ym').to_s+' months '+datedif(bd,today,'md').to_s+' days.'
Your age: 42 years 5 months 26 days.=> nil
>> print '今日で'+datedif(bd,today,'y').to_s+'歳'+datedif(bd,today,'ym').to_s+'ヶ月'+datedif(bd,today,'md').to_s+'日です。'
今日で42歳5ヶ月26日です。=> nil

とりあえず動いている様子。

こんな面倒な年齢の「○歳○ヶ月○日」や在職期間の「○年○ヶ月○日」といった表示の計算がExcelではDatedifという簡単な式として定義されています。さっすがExcelですね。、、と思いきや結構バグだらけな様子。確かに手元にあるExcell 2002(Windows)でも

=DATEDIF("2003/10/31","2003/11/10","md") → 10 #本当は10日間。これは合っている。
=DATEDIF("2003/10/31","2003/12/10","md") → 9  #本当は10日間。間違い。
=DATEDIF("2008/4/30","2009/3/1","md") → -1 #マ、マイナス?

>> datedif(Date.new(2003,10,31), Date.new(2003,12,10),'md')
=> 10 #合っている
>> datedif(Date.new(2008,4,30), Date.new(2009,3,1),'md')
=> 1 #合っている

ここにバグが良くまとまっているが結構きている。おかしいのはMDとYDなので実害が少ないということで放置なのだろうか。

ちなみにExcelのdatedifの引数に上記のメソッドは合わせました。

DATEDIF(開始日,終了日,単位)
この日から(開始日)
この日までを(終了日)
この単位で(単位)
単位:
"Y"	指定した期間の年数(満年数)を表示
"M"	指定した期間の月数(満月数)を表示
"D"	指定した期間の日数(満日数)を表示
"YM"	指定した期間の1年に満たない月数を表示
"MD"	指定した期間の1ヶ月に満たない日数を表示
"YD"	指定した期間の1年に満たない日数を表示
例:=DATEDIF("1999/3/1","2000/2/29","YM") → 11

VBのdatediffというのはExcelのdatedifとは異なり、「満年齢、満月齢、端数の日数」という計算は出来ない。

それ以上の間隔 Interval が DateInterval.Year に設定されていると、戻り値は単純に Date1 の年と Date2 の年から求められます。同様に、DateInterval.Month の戻り値は、引数の年と月の部分から求められ、DateInterval.Quarter の戻り値は、2 つの日付を含む四半期から求められます。たとえば、12 月 31 日と翌年の 1 月 1 日を比較する場合、差は 1 日ですが、DateDiff は DateInterval.Year、DateInterval.Quarter、DateInterval.Month に対して 1 を返します。

記載されています。

Google Spreadsheetにもdatedif関数は実装されていませんでした。

PerlにはTimeDate->delta_days, TimeDate->delta_monthsという実装があり、端数の日付取得もできるようです。

年齢以外の応用には「期間の定義」自体が色々あるようで慎重になったほうが良いようです。

「Rubyで満年齢、満月齢を(ExcelのDatedif)」への1件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA