kt関数アドイン/ktPeriodYMD関数で使っている期間計算マクロ([1a]:民法規定による算出)
です。モーグ(Eccel/VBA ,2003/4)で公開しましたので、ここでも公開します。活用して下さい。
※ Excel : DATEDIF 関数 の バグ について ※ (1) Excel 発売当初から存在している バグ [ 詳細 ( 1 ) , ( 2 ) ] 閏年の2/29 に絡んで YD & MD の算出結果に誤り ( 時にはマイナスになります ) があります。
しかし、Lotus 1-2-3 との互換性維持の為に用意されたという背景から DATEDIF は [ 隠し関数 ] の
扱いとなっており、その位置付け故に Microsoft は積極的に修正するという姿勢を見せていません。
Excel2010 でも、このバグは健在です。 Excel の中の実装ロジック
(2) Excel2007 での算出結果が [ Excel2003以前&Excel2010 ] と異なる バグ [ 詳 細 ] Excel2007 で施された変更によって、YD & MD の算出に、使用に耐えられない程の著しい ミス が
増えました。その後、[ SP2 ] [ KB 969682 ] と2回の修正が試みられ、かなり改善(上記 (1) の バグ を
含んだ状態の Excel2003以前の算出に、ほぼ戻った) されましたが、未だ一部で計算ミス が残っています。
MD には [ 終了日が 閏年&1月 ] 限定で 100 を超える値を返すバグが残っています。
2012/2/20 : Excel2007(SP3) の DATEDIF 不具合に関する 技術情報が公開されました ( KB2678753 )
2014/2/12 : Excel2007(SP3) で 修正アップデート KB2827324 (2013/10/8 リリース) の適用によって、
上記で紹介した補正値(誤差)が変わっている事が判明しました。
2014/3/7 : この デグレード に伴い MS-KB ( KB2678753 )が修正されました。
( 別ページ ) 期間計算の考え方
(1) JavaScript による日数計算シミュレーター
(2) 期間計算マクロ ktDATEDIF (初日算入オプション付き バージョン)
(3) 〃 ktDATEDIF (初日不算入 ベーシック バージョン)
(4) 〃 ktDATEDIF_SPLIT (暦月と両端の端数日数を分けて計算) ( 2023/10/11 追記 )
(5) [M:期間内満総月数] の使い方について
(6) 期間計算の民法規定
(7) 年比率の考え方 : 抽象的2月29日説 / 具体的2月29日説
(8) Excel YEARFRAC (基準:1 Act/Act )
(9) YEARFRAC (基準:1 Act/Act ) で年齢計算する危うさについて
(10) OOo.Calc YEARFRAC ( 基準:1 Act/Act )
(11) Act/Act ( AFB ) の計算方式
(12) Google SpreadSheet YEARFRAC ( 基準:1 Act/Act)
(13) Excel2007 で増えた DATEDIF 関数の不具合の紹介 , Excel2007[YD / MD] ロジックの考察
-------------------------------------------------------------------------
引数『開始日』『終了日』に対し、民法規定に従って【y年mヶ月d日】を求めるマクロです。
ktDATEDIFの基本計算ルールは「初日不算入」ですが、年齢計算の際には『初日算入』と
する必要があります(民法 期間計算の原則)。
初日算入機能あり Ver. -- FirstDay オプションを True にする。
ktDATEDIF(誕生日, 現在日付, "Y", True)
初日算入機能なし Ver. -- 「終了日」 に 『+1』する。
ktDATEDIF(誕生日, 現在日付 + 1, "Y")
[ 2005/4/17 ]
今まで、計算部分のマクロだけの掲載(2003/4/23〜)でしたが、「関数」仕立てにしてDATEDIF関数の代用となるようにしました。
尚、[y年mヶ月d日]編集 と [初日算入/不算入]指定 の機能を付け足してあります。
[ 2005/4/18 ]
Excel/VBA だけでなく、OpenOffice.org [Calc] Basic でも利用できるように、日本語変数名を止めました。
下記のマクロは、VBA および OOoBasic で動作します。
[ 2008/3/22 ]
YD (年未満の端数日数) の計算機能を加えました。また、Excel 向けに省略引数仕様にしました。
[ 2008/10/24 ]
年比率( YEARFRAC ) の計算オプションを加えました。
[ 2008/10/26 ]
日付計算のシミュレーションを加えました。
[ 2010/5/30 ]
Google SpreadSheet の YEARFRAC(Act/Act) の調査を行ないました。
[ 2012/1/17 ]
ktDATEDIF (初日算入 , 初日不算入 両バージョン) 内の [ intMonth As Integer ] を [ lngMonth As Long ] に修正する。
理由: 2日付の間隔が 2730年 を超えると オーバーフローにより #VALUE となる為
[ 2019/4/14 ]
[ Excel 2007 以前 ] 等、新元号[ 令和 ] 未対応環境 において、日付引数に "令和1年5月1日" 等の日付文字列を
指定可能にする修正方法を追記しました。
[ この場所へのリンク ]
(お願い) Q&Aサイトの掲示板等に下記 マクロ コード を丸々貼り付ける事は御遠慮願います。
この マクロ コード を紹介される場合は、上記リンク URL の提示にてお願いします。
[初日算入] 機能を省いたバージョンは こちら
-- [初日算入] 機能あり バージョン ---
※ 新元号 [ 令和 ] 対応のアップデートが施されている環境では、下記 マクロコード のままで
引数 ( SatrtDate / EndDate ) に "令和1年5月1日" 等の日付文字列 を指定可能です。
未アップデート環境で 「令和日付」 に対応する為の修正方法は後述。
'_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
'_/
'_/ ----- DATEDIF の代替関数 [ ktDATEDIF ( 民法準拠 ) ] -----
'_/ [ 初日算入オプション ] 拡張バージョン
'_/
'_/ [ http://addinbox.sakura.ne.jp/Excel_Tips05.htm#ktDATEDIF1 ]
'_/
'_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
Public Function ktDATEDIF(ByVal StartDate As Date, _
ByVal EndDate As Date, _
Optional ByVal Interval As String = "YMD", _
Optional ByVal FirstDay As Boolean = False) As Variant
' OOo.BasicではOptionalでの初期値が指定できない
' ByVal Interval As String, _
' ByVal FirstDay As Boolean) As Variant
' [ http://addinbox.sakura.ne.jp/Excel_Tips05.htm#ktDATEDIF1 ]
'
' [Interval]
' YMD:"y年mヶ月d日"で編集(文字列)
' Y :期間内満年数
' M :期間内満総月数
' YM:年未満の端数の月数
' MD:月未満の端数の日数
' YD:年未満の端数の日数
' FR:年比率(Double型, YEARFRACモード)
' [FirstDay]
' 初日算入=True, 初日不算入=False (民法規定は[初日 不算入]が基本)
'(注)
' Interval="FR"は、YEARFRAC関数の[基準:1(Actual/Actual)]に相当しますが、
' YEARFRACとは計算方法が異なり、『抽象的2月29日説』に基づいています。
' 実務規則に合致しているかを確認してから利用して下さい。
' (例)
' 2003/5/10 - 2007/6/20 → 4年41日間 → 4.1120218579 ( = 41/366)
' [2003/5/10(ex) - 2007/5/10(in)]で4年間。[2007/5/11(in) - 6/20(in)]で41日間。
' [2007/5/11(in) - 6/20 - 2008/5/10(in)]の間に2/29があるので366日/年。
'
' 2003/5/10 - 2008/6/20 → 5年41日間 → 5.1123287671 ( = 41/365)
' [2003/5/10(ex) - 2008/5/10(in)]で5年間。[2008/5/11(in) - 6/20(in)]で41日間。
' [2008/5/11(in) - 6/20 - 2009/5/10(in)]の間に2/29がないので365日/年。
Dim dtmFirstDate As Date '起算日
Dim dtmLastDate As Date '満了日
Dim wkDate As Date
Dim lngMonth As Long '総月数(13ヶ月 など)
Dim intYear As Integer
Dim intMonth_in_Year As Integer '年未満の月数
Dim intDay_in_Month As Integer '月未満の日数
Dim intDay_in_Year As Integer '年未満の日数
Dim dblFrac As Double
Dim intFracDays As Integer '[年未満の日数]部分の初日から1年間の日数
'(その1年間に2/29がある→366、ない→365)
If (StartDate > EndDate) Then
ktDATEDIF = CVErr(xlErrValue)
'ktDATEDIF = "Error" 'OOo.BasicではCVErr(xlErrValue)が使えない
Exit Function
End If
'起算日の設定
If (FirstDay = False) Then
dtmFirstDate = StartDate + 1 '民法規定は[初日 不算入]が基本
Else
dtmFirstDate = StartDate 'True:[初日算入]指定
End If
dtmLastDate = EndDate
'注)[DateDiff関数]自体は初日不算入で処理している
' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If (Day(dtmFirstDate) = 1) Then
'起算日が1日(開始日が月末日)の場合、月の大小閏に係わりなく
'[月末日]までで『丸Nヶ月』
'月の大小閏に影響されないように「月末日〜終了日」を1日分シフトして
'「1日〜(終了日+1)」で求める。
lngMonth = DateDiff("m", dtmFirstDate, (dtmLastDate + 1))
intYear = lngMonth \ 12
intMonth_in_Year = lngMonth Mod 12
If (Day(dtmLastDate + 1) = 1) Then '満了日(終了日)が月末か?
intDay_in_Month = 0
Else
intDay_in_Month = Day(dtmLastDate)
End If
wkDate = DateAdd("yyyy", intYear, dtmFirstDate)
intDay_in_Year = DateDiff("d", wkDate, (dtmLastDate + 1))
If (intDay_in_Year > 0) Then
intFracDays = DateAdd("yyyy", 1, wkDate) - wkDate '365 or 366
End If
Else
'起算日≠1日の場合、「起算日の応当日前日」までで『丸Nヶ月』
lngMonth = DateDiff("m", (dtmFirstDate - 1), dtmLastDate)
wkDate = DateAdd("m", lngMonth, (dtmFirstDate - 1))
If (wkDate > dtmLastDate) Then
lngMonth = lngMonth - 1
wkDate = DateAdd("m", lngMonth, (dtmFirstDate - 1))
End If
intDay_in_Month = DateDiff("d", wkDate, dtmLastDate)
intYear = lngMonth \ 12
intMonth_in_Year = lngMonth Mod 12
wkDate = DateAdd("yyyy", intYear, (dtmFirstDate - 1))
intDay_in_Year = DateDiff("d", wkDate, dtmLastDate)
If (intDay_in_Year > 0) Then
intFracDays = DateAdd("yyyy", 1, wkDate) - wkDate '365 or 366
End If
End If
' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select Case UCase(Interval)
Case "YMD"
ktDATEDIF = intYear & "年" & intMonth_in_Year & "ヶ月" & intDay_in_Month & "日"
Case "Y"
ktDATEDIF = intYear
Case "M"
ktDATEDIF = lngMonth
Case "YM"
ktDATEDIF = intMonth_in_Year
Case "MD"
ktDATEDIF = intDay_in_Month
Case "YD"
ktDATEDIF = intDay_in_Year
Case "FR"
If (intDay_in_Year > 0) Then
dblFrac = intDay_in_Year / CDbl(intFracDays) '整数演算とならないように型変換
ktDATEDIF = CDbl(intYear + dblFrac)
Else
ktDATEDIF = CDbl(intYear) '年未満の日数が無いので小数は出ない
End If
Case Else
ktDATEDIF = CVErr(xlErrValue)
'ktDATEDIF = "Error" 'OOo.BasicではCVErr(xlErrValue)が使えない
End Select
End Function
※ [ Excel 2007 以前 ] 等、新元号[ 令和 ] 未対応の環境で 令和日付文字列を指定可能にする修正方法
新元号対応 日付変換関数 の マクロコード [ NewEraFunction.bas ] をインポートで組み込んだ後、
下記 赤字 部分を追加/修正してください。
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
Public Function ktDATEDIF(ByVal StartDate As Variant, _
ByVal EndDate As Variant, _
Optional ByVal Interval As String = "YMD", _
Optional ByVal FirstDay As Boolean = False) As Variant
:
Dim dtmStartDate As Date '日付引数の型変換(StartDate)
Dim dtmEndDate As Date '日付引数の型変換(EndDate)
Dim dtmFirstDate As Date '起算日
Dim dtmLastDate As Date '満了日
:
Dim intFracDays As Integer '[年未満の日数]部分の初日から1年間の日数
'(その1年間に2/29がある→366、ない→365)
If EraIsDate(StartDate) And EraIsDate(EndDate) Then
dtmStartDate = EraCDate(StartDate)
dtmEndDate = EraCDate(EndDate)
Else
ktDATEDIF = CVErr(xlErrValue)
'ktDATEDIF = "Error" 'OOo.BasicではCVErr(xlErrValue)が使えない
Exit Function
End If
If (dtmStartDate > dtmEndDate) Then
ktDATEDIF = CVErr(xlErrValue)
'ktDATEDIF = "Error" 'OOo.BasicではCVErr(xlErrValue)が使えない
Exit Function
End If
'起算日の設定
If (FirstDay = False) Then
dtmFirstDate = dtmStartDate + 1 '民法規定は[初日 不算入]が基本
Else
dtmFirstDate = dtmStartDate 'True:[初日算入]指定
End If
dtmLastDate = dtmEndDate
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[ この場所へのリンク ]
[初日算入] 機能を盛り込んだ為に、dtmFirstDate で「1日足したのに、また1日引いて」という
ロジックになっています。この辺りの意味が良く理解できないという方の為に、[初日算入]機能を
省いたバージョンを用意しましたので、理解する上での助けとしてください。(2008/3/24 追記)
(お願い) Q&Aサイトの掲示板等に下記 マクロ コード を丸々貼り付ける事は御遠慮願います。
この マクロ コード を紹介される場合は、上記リンク URL の提示にてお願いします。
-- [初日算入] 機能を省いたバージョン --- [ シミュレーション ]
※ 新元号 [ 令和 ] 対応のアップデートが施されている環境では、下記 マクロコード のままで
引数 ( SatrtDate / EndDate ) に "令和1年5月1日" 等の日付文字列 を指定可能です。
未アップデート環境で 「令和日付」 に対応する為の修正方法は後述。
'_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
'_/
'_/ ----- DATEDIF の代替関数 [ ktDATEDIF ( 民法準拠 ) ] -----
'_/
'_/ [ http://addinbox.sakura.ne.jp/Excel_Tips05.htm#ktDATEDIF2 ]
'_/
'_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
Public Function ktDATEDIF(ByVal StartDate As Date, _
ByVal EndDate As Date, _
ByVal Interval As String) As Variant
' [ http://addinbox.sakura.ne.jp/Excel_Tips05.htm#ktDATEDIF2 ]
'
' [Interval]
' YMD :"y年mヶ月d日"で編集(文字列)
' Y :期間内満年数
' M :期間内満総月数
' YM :年未満の端数の月数
' MD :月未満の端数の日数
' YD :年未満の端数の日数
' FR :年比率(Double型, YEARFRACモード)
'(注)
' Interval="FR"は、YEARFRAC関数の[基準:1(Actual/Actual)]に相当しますが、
' YEARFRACとは計算方法が異なり、『抽象的2月29日説』に基づいています。
' 実務規則に合致しているかを確認してから利用して下さい。
' (例)
' 2003/5/10 - 2007/6/20 → 4年41日間 → 4.1120218579 ( = 41/366)
' [2003/5/10(ex) - 2007/5/10(in)]で4年間。[2007/5/11(in) - 6/20(in)]で41日間。
' [2007/5/11(in) - 6/20 - 2008/5/10(in)]の間に2/29があるので366日/年。
'
' 2003/5/10 - 2008/6/20 → 5年41日間 → 5.1123287671 ( = 41/365)
' [2003/5/10(ex) - 2008/5/10(in)]で5年間。[2008/5/11(in) - 6/20(in)]で41日間。
' [2008/5/11(in) - 6/20 - 2009/5/10(in)]の間に2/29がないので365日/年。
Dim wkDate As Date
Dim lngMonth As Long '総月数(13ヶ月 など)
Dim intYear As Integer
Dim intMonth_in_Year As Integer '年未満の月数
Dim intDay_in_Month As Integer '月未満の日数
Dim intDay_in_Year As Integer '年未満の日数
Dim dblFrac As Double
Dim intFracDays As Integer '[年未満の日数]部分の初日から1年間の日数
'(その1年間に2/29がある→366、ない→365)
If (StartDate > EndDate) Then
ktDATEDIF = CVErr(xlErrValue)
'ktDATEDIF = "Error" 'OOo.BasicではCVErr(xlErrValue)が使えない
Exit Function
End If
'注)[DateDiff関数]自体は初日不算入で処理している
' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If (Day(StartDate + 1) = 1) Then
'起算日が1日(開始日が月末日)の場合、月の大小閏に係わりなく
'[月末日]までで『丸Nヶ月』
'月の大小閏に影響されないように「月末日〜終了日」を1日分シフトして
'「1日〜(終了日+1)」で求める。
lngMonth = DateDiff("m", (StartDate + 1), (EndDate + 1))
intYear = lngMonth \ 12
intMonth_in_Year = lngMonth Mod 12
If (Day(EndDate + 1) = 1) Then '満了日(終了日)が月末か?
intDay_in_Month = 0
Else
intDay_in_Month = Day(EndDate)
End If
wkDate = DateAdd("yyyy", intYear, (StartDate + 1)) '(StartDate+1)は[1日]
intDay_in_Year = DateDiff("d", wkDate, (EndDate + 1))
If (intDay_in_Year > 0) Then
intFracDays = DateAdd("yyyy", 1, wkDate) - wkDate '365 or 366
End If
Else
'起算日≠1日の場合、「起算日の応当日前日」までで『丸Nヶ月』
lngMonth = DateDiff("m", StartDate, EndDate)
wkDate = DateAdd("m", lngMonth, StartDate)
If (wkDate > EndDate) Then
lngMonth = lngMonth - 1
wkDate = DateAdd("m", lngMonth, StartDate)
End If
intDay_in_Month = DateDiff("d", wkDate, EndDate)
intYear = lngMonth \ 12
intMonth_in_Year = lngMonth Mod 12
wkDate = DateAdd("yyyy", intYear, StartDate)
intDay_in_Year = DateDiff("d", wkDate, EndDate)
If (intDay_in_Year > 0) Then
intFracDays = DateAdd("yyyy", 1, wkDate) - wkDate '365 or 366
End If
End If
' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select Case UCase(Interval)
Case "YMD"
ktDATEDIF = intYear & "年" & intMonth_in_Year & "ヶ月" & intDay_in_Month & "日"
Case "Y"
ktDATEDIF = intYear
Case "M"
ktDATEDIF = lngMonth
Case "YM"
ktDATEDIF = intMonth_in_Year
Case "MD"
ktDATEDIF = intDay_in_Month
Case "YD"
ktDATEDIF = intDay_in_Year
Case "FR"
If (intDay_in_Year > 0) Then
dblFrac = intDay_in_Year / CDbl(intFracDays) '整数演算とならないように型変換
ktDATEDIF = CDbl(intYear + dblFrac)
Else
ktDATEDIF = CDbl(intYear) '年未満の日数が無いので小数は出ない
End If
Case Else
ktDATEDIF = CVErr(xlErrValue)
'ktDATEDIF = "Error" 'OOo.BasicではCVErr(xlErrValue)が使えない
End Select
End Function
※ [ Excel 2007 以前 ] 等、新元号[ 令和 ] 未対応の環境で 令和日付文字列を指定可能にする修正方法
新元号対応 日付変換関数 の マクロコード [ NewEraFunction.bas ] をインポートで組み込んだ後、
下記 赤字 部分を追加/修正してください。
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
Public Function ktDATEDIF(ByVal StartDate As Variant, _
ByVal EndDate As Variant, _
Optional ByVal Interval As String = "YMD", _
Optional ByVal FirstDay As Boolean = False) As Variant
:
Dim dtmStartDate As Date '日付引数の型変換(StartDate)
Dim dtmEndDate As Date '日付引数の型変換(EndDate)
Dim wkDate As Date
:
Dim intFracDays As Integer '[年未満の日数]部分の初日から1年間の日数
'(その1年間に2/29がある→366、ない→365)
If EraIsDate(StartDate) And EraIsDate(EndDate) Then
dtmStartDate = EraCDate(StartDate)
dtmEndDate = EraCDate(EndDate)
Else
ktDATEDIF = CVErr(xlErrValue)
'ktDATEDIF = "Error" 'OOo.BasicではCVErr(xlErrValue)が使えない
Exit Function
End If
If (dtmStartDate > dtmEndDate) Then
ktDATEDIF = CVErr(xlErrValue)
'ktDATEDIF = "Error" 'OOo.BasicではCVErr(xlErrValue)が使えない
Exit Function
End If
( 以下、StartDate ⇒ dtmStartDate , EndDate ⇒ dtmEndDate に全て置換する )
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[ この場所へのリンク ]
(お願い) Q&Aサイトの掲示板等に下記 マクロ コード を丸々貼り付ける事は御遠慮願います。
この マクロ コード を紹介される場合は、上記リンク URL の提示にてお願いします。
(補) これは kt関数アドイン / ktPeriodYMD ( 2a ) でサポートしている算出方法 の 仕様 と ロジックコード です。
リリース 時には 「こういう算出もありえるかも?」 と思いつつ実装しましたが、正直、実務としては九分九厘無いだろうとも思ってました。
しかし、YAHOO知恵袋に実際に 実務で使っているらしき投稿 がありましたので、お役に立てればと、仕様/ロジックコード を公開する
事としました。その投稿を見付けた事に対する 私の投稿は こちら です。
-- [ ktDATEDIF_SPLIT ] 暦月と両端の端数日数を分けて計算 ---
'_/_/_/_/_/_/ ktDATEDIF_SPLIT 関数 _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
'_/
'_/ ----- 暦上の『月』と両端の『日』に分けて算出 -----
'_/ kt関数アドイン/ktPeriodYMD(2a)の期間計算
'_/
'_/ --+--------+--------+--------+--------+--------+--- 暦 ( + 〜 + が 1ヶ月 )
'_/ <------------------------------------> 計算期間
'_/ ****#========#--------#========#****** 暦の月で 1ヶ月カウント
'_/ この例では [ 3ヶ月 + ( 端数日数1 + 端数日数2 ) ]
'_/
'_/ 起算日〜満了日の間に挟まる暦上の【月】から年月数をカウントし、日数は
'_/ 「起算日〜起算月の月末」+「1日〜満了日」でカウントします。ただし、
'_/ 『起算の"日" ≦ 満了の"日"』の場合には「日数合計で1ヶ月を満たす」とし、
'_/ 月数1ヶ月を加え、日数は『起算日の応当日 〜 満了日』で求めます。
'_/
'_/ [ http://addinbox.sakura.ne.jp/Excel_Tips05.htm#ktDATEDIF_SPLIT ]
'_/
'_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
Public Function ktDATEDIF_SPLIT(ByVal StartDate As Variant, _
ByVal EndDate As Variant, _
Optional ByVal Interval As String = "YMD", _
Optional ByVal FirstDay As Boolean = False) As Variant
' OOo.BasicではOptionalでの初期値が指定できない
' ByVal Interval As String, _
' ByVal FirstDay As Boolean) As Variant
' [Interval]
' YMD:"y年mヶ月d日"で編集(文字列)
' Y :期間内満年数
' M :期間内満総月数
' YM:年未満の端数の月数
' MD:月未満の端数の日数
' [FirstDay]
' 初日算入=True, 初日不算入=False
Dim dtmFirstDate As Date '起算日
Dim dtmLastDate As Date '満了日
Dim dtmEndOfMonth_FirstDate As Date '[起算日の月]の月末日
Dim wkFirstDate As Date
Dim wkLastDate As Date
Dim lngMonth As Long '期間内の総月数(13ヶ月 など)
Dim intYear As Integer
Dim intMonth_in_Year As Integer '年未満の月数
Dim intDay_in_Month As Integer '月未満の日数
If (StartDate > EndDate) Then
ktDATEDIF_SPLIT = CVErr(xlErrValue)
'ktDATEDIF_SPLIT = "Error" 'OOo.BasicではCVErr(xlErrValue)が使えない
Exit Function
End If
'起算日/満了日の設定
If (FirstDay = False) Then
dtmFirstDate = StartDate + 1 'False:[初日不算入]指定
Else
dtmFirstDate = StartDate 'True:[初日算入]指定
End If
dtmLastDate = EndDate
' 起算日の月の月末日
dtmEndOfMonth_FirstDate = DateSerial(Year(dtmFirstDate), Month(dtmFirstDate) + 1, 0)
If (Format(dtmFirstDate, "yyyymm") = Format(dtmLastDate, "yyyymm")) Then
'起算日と満了日が同じ年月
If (Day(dtmFirstDate) = 1) And (Day(dtmLastDate + 1) = 1) Then
'[月初〜月末]なので『丸1ヶ月』丁度
intYear = 0
intMonth_in_Year = 1
intDay_in_Month = 0
Else
intYear = 0
intMonth_in_Year = 0
intDay_in_Month = Day(dtmLastDate) - Day(dtmFirstDate) + 1
End If
Else
'起算日と満了日が異なる年月
'----- 期間内の暦上の【月数】をカウント -------
' 日付を[??月1日〜??月1日]に調整してDateDiff(m)で月数を求める
' DateDiff関数は[初日不算入]なので上記の期間は[2日〜1日]の丸Nヶ月として認識される
If (Day(dtmFirstDate) = 1) Then
wkFirstDate = dtmFirstDate
Else
'起算日が中日なので翌月初日へシフト
wkFirstDate = DateSerial(Year(dtmFirstDate), (Month(dtmFirstDate) + 1), 1)
End If
If (dtmLastDate = DateSerial(Year(dtmLastDate), Month(dtmLastDate) + 1, 0)) Then
'満了日が月末日⇒翌月初日(1日)=翌日へシフト
wkLastDate = dtmLastDate + 1
Else
'満了日が中日⇒当月初日(1日)へシフト
wkLastDate = DateSerial(Year(dtmLastDate), Month(dtmLastDate), 1)
End If
If (wkFirstDate >= wkLastDate) Then
lngMonth = 0
Else
lngMonth = DateDiff("m", wkFirstDate, wkLastDate)
End If
'-------------------------------------------------
'----- 両端の端数日数の合計を算出 -----
If (Day(dtmFirstDate) = 1) And (Day(dtmLastDate + 1) = 1) Then
'[月初〜月末]なので『丸Nヶ月』⇒端数日数はゼロ
intDay_in_Month = 0
ElseIf (Day(dtmFirstDate) = 1) Then
'[月初〜α日]なので満了日側の端数日数のみ⇒ α
intDay_in_Month = Day(dtmLastDate)
ElseIf (Day(dtmLastDate + 1) = 1) Then
'[α日〜月末]なので起算日側の端数日数のみ⇒[α〜開始月の月末日]
intDay_in_Month = Day(dtmEndOfMonth_FirstDate) - Day(dtmFirstDate) + 1
ElseIf (Day(dtmFirstDate) <= (Day(dtmLastDate) + 1)) Then
'[β月10日〜γ月20日] [β月16日〜γ月15日]のように"端数日数"の合計で丸1ヶ月ある⇒月数をプラス1
lngMonth = lngMonth + 1
intDay_in_Month = Day(dtmLastDate) - Day(dtmFirstDate) + 1
Else
'[β月20日〜γ月10日]のように"端数日数"の合計は1ヶ月に満たない
intDay_in_Month = (Day(dtmEndOfMonth_FirstDate) - Day(dtmFirstDate) + 1) + Day(dtmLastDate)
End If
'総月数から[年数]と[年未満の端数の月数]を求める
intYear = lngMonth \ 12
intMonth_in_Year = lngMonth Mod 12
End If
' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select Case UCase(Interval)
Case "YMD"
ktDATEDIF_SPLIT = intYear & "年" & intMonth_in_Year & "ヶ月" & intDay_in_Month & "日"
Case "Y"
ktDATEDIF_SPLIT = intYear
Case "M"
ktDATEDIF_SPLIT = lngMonth
Case "YM"
ktDATEDIF_SPLIT = intMonth_in_Year
Case "MD"
ktDATEDIF_SPLIT = intDay_in_Month
Case Else
ktDATEDIF_SPLIT = CVErr(xlErrValue)
'ktDATEDIF_SPLIT = "Error" 'OOo.BasicではCVErr(xlErrValue)が使えない
End Select
End Function
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ktDATEDIF_SPLIT と ktDATEDIF(民法準拠) との 算出結果の違い
------- 初日算入 ------- ------- 初日不算入 ------- ktDATEDIF_SPLIT ktDATEDIF ktDATEDIF_SPLIT ktDATEDIF 2023/3/16 〜 2023/4/30 0年1ヶ月16日 0年1ヶ月15日 0年1ヶ月15日 0年1ヶ月14日 2023/3/16 〜 2023/4/29 0年1ヶ月14日 0年1ヶ月14日 0年1ヶ月13日 0年1ヶ月13日 2023/3/16 〜 2023/3/31 0年0ヶ月16日 0年0ヶ月16日 0年0ヶ月15日 0年0ヶ月15日 2023/4/ 1 〜 2023/4/30 0年1ヶ月0日 0年1ヶ月0日 0年0ヶ月29日 0年0ヶ月29日 2023/4/16 〜 2023/4/30 0年0ヶ月15日 0年0ヶ月15日 0年0ヶ月14日 0年0ヶ月14日 2023/4/ 2 〜 2023/4/30 0年0ヶ月29日 0年0ヶ月29日 0年0ヶ月28日 0年0ヶ月28日 2023/3/16 〜 2024/2/28 0年11ヶ月13日 0年11ヶ月13日 0年11ヶ月12日 0年11ヶ月12日 2023/3/16 〜 2024/2/29 0年11ヶ月16日 0年11ヶ月14日 0年11ヶ月15日 0年11ヶ月13日 2023/3/25 〜 2024/3/10 0年11ヶ月17日 0年11ヶ月15日 0年11ヶ月16日 0年11ヶ月14日 2022/3/25 〜 2023/3/10 0年11ヶ月17日 0年11ヶ月14日 0年11ヶ月16日 0年11ヶ月13日
[ この場所へのリンク ]
---- [M:期間内満総月数] の使い方について ----
"y年mヶ月" 同士の加減算を行なう場合、文字列の"y年mヶ月"のままでは加減算が
出来ないので、数値である「満総月数(1年1ヶ月なら 13ヶ月)」で加減算を行ない、
結果の月数に対して『12による商と剰余』を求め「年と月」に編集します。
Excel : =INT(月数の結果/12) & "年" & MOD(月数の結果,12) & "ヶ月"
VBA : (月数の結果 \ 12) & "年" & (月数の結果 Mod 12) & "ヶ月"
尚、"y年mヶ月d日"同士の加減算は、『算出の定義』が明確になっていない限りは、
無意味です(何日で1ヶ月と勘定するのか等)。
-------------------------------------------------------------------
期間計算を行うにあたっては、「その期間計算で処理する業務」に係わる当事者間で、
定義されている算出方法とは「何か?」が一番大事です(仕様に合っていないプログラム
を作っても、それで得られる結果は、その業務では役に立たない)。
例えば、「民法の定義に従って求める」、「初日/末日の算入/不算入はこうする」、
「入社月は途中入社でも丸1ヶ月とカウントする」、「1ヶ月は30日間で計算する」といった
個々の業務特有の条件などがありますので、その辺を良く検討する必要があります。
一般的には、特に定めが明記されていない場合の算出基準は『民法に従う』と解釈
されています。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[ この場所へのリンク ]
民法における、期間の正式な定義についてはこちらのページを参考にして下さい。
民法規定 を判り易く整理すると下記のようになります。 (2008/3/25 追記)
a ) 「期間の初日」 はカウントしない。即ち、引数:開始日に指定した日付はカウントせず、その翌日
からカウントする。
b ) 「1日」 からカウントする場合は(即ち、引数:開始日に月末日を指定)、月の大小閏に係わりなく、
月末日までで 「満 n ヶ月」、開始日と同月の月末日までで 「満 n 年」 となる。
c ) 「1日」 以外からカウントする場合は(即ち、引数:開始日が月末日でない)、開始日と「同日」 まで
で 「満 n ヶ月」、開始日と 「同月日」 までで 「満 n 年」 となる。但し、月の大小閏の関係で対応す
る 「同日」 「同月日」 が存在しない場合は、月末日までで 「満 n ヶ月」 「満 n 年」 となる。
補足 ) DATEDIF 引数: Y , M , YM , MD , YD の意味
上記仕様で計算される 「満 n ヶ月( 13ヶ月 など )」 が [M]の月数。それを12で割った商が [Y] の
年数で、余りが [YM] の月数。
「満 n ヶ月」 となる日付の翌日から [引数:終了日] までの日数が [MD] の日数。
「満 n 年」 となる日付の翌日から[引数:終了日] までの日数が [YD] の日数。
(注) DATEDIF は上記仕様とは完全には一致していません(異なる結果を返す。DATEDIF のバグ )。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[ この場所へのリンク ]
『抽象的 2月 29日説』 および 『具体的 2月 29日説』 について
YEARFRAC 関数で算出される 「期間日数の年比率」 は、Excel が米国製ソフトということもあり、欧米圏において
使われている方式 ( 30/360方式 など ) で計算されます。
「基準:1 ( 実際の日数/実際の日数 )」 は、括弧内の表現からカレンダー通りに計算されて日本でも通用するよう
に見えますが、日本で一般的な計算方式とは異なる計算を行っていますので、これもまた日本では使わない方が
良いでしょう。
日本においては、 『抽象的 2月 29日説』 という方式が一般的です。詳細は下記を参照して下さい。
ILCジャーナル / 「金利及び弁済金額計算に関する法律と実務」抜粋
『金利計算と閏年・端数処理について』 大阪弁護士会所属 弁護士 服部 廣志
http://www.ilc.gr.jp/journal/000109/ (by Internet Archive )
補:インターネット法律協議会 ( ILC ) - 2017年5月31日 閉鎖 -
難しく書かれていますが、『抽象的2月29日説』 を要約すると以下のように計算します。
a) [開始日〜終了日] から [ y 年 と d 日間 ] という期間を算出する(The remainning final stub period )。
b) 年比率 = y + ( d ÷ 年間日数[365 or 366] ) で比率を計算する。
c) 365 と 366 の何れで割るかの判断は、半端日数部分 (d日間) の初日から見て、向こう1年間の間に
2月29日が有れば 366、無ければ 365 で割ります(向こう1年間の年間日数で割るという事です)。
2003/5/10 〜 2007/6/20 → 4年 と 41日間 , 4.1120218579
2003/5/10 〜 2008/6/20 → 5年 と 41日間 , 5.1123287671
前者は、2007/5/11[含] 〜 6/20 〜 2008/5/10 の間に 2/29 が有るので [ 41÷366 ]
後者は、2008/5/11[含] 〜 6/20 〜 2009/5/10 の間に 2/29 が無いので [ 41÷365 ]
尚、『具体的 2月29日説』 は「半端日数部分の期間内に 2月29日が有るか無いか」 で判断する点が
異なります。2006/3/1 〜 2007/3/10 の場合、
[2006/3/1 〜 2007/2/28] で1年、[2007/3/1[含]〜2007/3/10] が残りの半端期間(10日間)
抽象的 の方では [ 10 ÷ 366 ] ですが、具体的 の方では [ 10 ÷ 365 ] です。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[ この場所へのリンク ]
『Excel YEARFRAC 基準:1 (Actual/Actual)』 について
[Actual /Actual ] と記される、実日数で年比率を計算する欧米圏の算出方式にはISDA , ISMA , AFB など
幾つかの方式が規定されていますが、Excel の YEARFRAC (基準:1 Actual / Actual ) が行っている計算は
これらの何れとも合致しません。
Microsoft に問い合わせたところ、何故この計算方式を採ったのかという理由は、現在 Microsoft 自体でも判ら
ないとの事です ( 2008/10/22 ) 。
仕様書がこのような計算手順を指示したのか、仕様が曖昧な為にプログラマーが仕様の曖昧部分を補完しつ
つ独自にロジックを考えてプログラムしたものなのか?
YEARFRAC の算出結果から調査したところ、以下の手順で計算を行っていると判明しました。
(1) 開始日と終了日が 同一年 の場合
その年の日数(365 or 366)で割る
(2) 開始日〜終了日が 複数年 に跨る場合
開始日〜終了日の期間(開始日除く)が
(a) 1年以下の場合
開始日〜終了日(開始日も含む) に[2/29] が含まれれば 366、無ければ 365 で期間日数を割る
(b) 1年超の場合
期間の跨る全ての年(開始日=12/31の場合でも開始年を含む) から[ 年間日数(365 or 366) ]の
平均を求め、その値(365.25 , 365.33… 等)で期間日数を割る
※ (a)(b)下線の部分は、初日不算入(開始日そのものは期間に含まれない) の条件を考えれば明らかに
間違った処理(余計なカウント)です。
【 Excel YEARFRAC (基準:1 Actual/Actual ) の 実装ロジック 】 [ シミュレーション ]
Public Function YEARFRAC_Act(ByVal StartDate As Date, ByVal EndDate As Date) As Double
Dim wkDate As Date
Dim dtmLeapDay As Date
Dim dblDaysYear As Double
Dim lngDays As Long
wkDate = DateAdd("yyyy", 1, StartDate)
If (Year(StartDate) = Year(EndDate)) Then
'開始日/終了日が同一年
If (Month(DateSerial(Year(StartDate), 2, 29)) = 2) Then
dblDaysYear = 366
Else
dblDaysYear = 365
End If
ElseIf (EndDate <= wkDate) Then
'年跨りで1年以下の期間
'(開始日[含]〜終了日に[2/29]が有れば366、無ければ365で割る)
If (Month(DateSerial(Year(StartDate), 2, 29)) = 2) Then
'閏年(年内) or 閏年→平年
dtmLeapDay = DateSerial(Year(StartDate), 2, 29)
If (StartDate <= dtmLeapDay) And (dtmLeapDay <= EndDate) Then
dblDaysYear = 366
Else
dblDaysYear = 365
End If
ElseIf (Month(DateSerial(Year(EndDate), 2, 29)) = 2) Then
'平年→閏年
dtmLeapDay = DateSerial(Year(EndDate), 2, 29)
If (StartDate <= dtmLeapDay) And (dtmLeapDay <= EndDate) Then
dblDaysYear = 366
Else
dblDaysYear = 365
End If
Else
'平年→平年
dblDaysYear = 365
End If
Else
'1年超の期間(経過年の平均[年間日数]で割る)
lngDays = DateSerial(Year(EndDate), 12, 31) - DateSerial(Year(StartDate), 1, 0)
dblDaysYear = lngDays / (Year(EndDate) - Year(StartDate) + 1)
End If
YEARFRAC_Act = (EndDate - StartDate) / dblDaysYear
End Function
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[ この場所へのリンク ]
『YEARFRAC(基準:1) で 年齢計算 する危うさ 』 について
年齢 を算出する 対象日(引数の終了日) が
1. 誕生日(引数の開始日の月日)の 『前後 2日 程』 の場合
YEARFRAC 関数は 月の大小閏の影響により、正しい期間(満年数) を算出できない
(1〜2日 足りない or 多い) という ケース があります。
誕生日近辺では如何に小数部を処理 (切捨 / 切上 / 丸め) しても、
正しい年齢(満年数) が 常に得られるとは限りません
2. 上記以外 : 誕生日(引数の開始日の月日)よりも 『 3日以降』 の場合
この辺りからは算出される期間が確実に 『正しい満年数+α日間』 となりますので、算出結果の
小数部を 切捨 (INT or ROUNDDOWN ) することで正しい年齢が得られます。
- - ( 解 説 )- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
多くの Excel 解説サイトや Q & A サイト で、YEARFRAC(基準:1) を使って年齢計算する方法が紹介
されています (時には、[ 基準:0 (NASD) ] を使っていることさえあります) 。計算結果には期待するも
のと小数部分でのズレが出る為に、INT/ROUND/ROUNDUP/ROUNDDOWN 関数を併用して紹介し
ている場合が多いですね。
しかし、これら丸め関数との併用を行なっても、YEARFRAC では常に正しい年齢の計算ができるとは
限りません。下記の例で説明します (ここでは、誕生日の日に1歳増するものとしています。
法律通りに誕生日の前日で1歳増とする場合には =YEARFRAC(開始日, 終了日 + 1, 1) とします )。
年齢 YEARFRAC INT ROUND ROUNDUP ROUNDDOWN
2004/1/30〜2017/1/30 13 13.0007821666 13 13 14 13
2004/3/30〜2018/3/30 14 13.9979923344 13 14 14 13
-----------------------------------------------------------------------
2004/3/30〜2018/3/29 13 13.9952546085 13 14 14 13
2004/3/30〜2018/3/31 14 14.0007300602 14 14 15 14
前2つでは、丁度 「誕生日の日」 になっていますので、各々13歳・14歳と計算される筈ですが、どち
らにも小数部分のズレが出ています。INT/ROUNDUP/ROUNDDOWN では両方とも正しい年齢を得
ることは出来ていません。ROUND関数による四捨五入では上手くいっているようです。
このケースから 「ROUND関数と併用すれば問題なし」 と決めてしまうのは早計です。丁度誕生日に
なる日にだけ計算する訳ではありません。誕生日以外の日で計算する場合もあるのです。
後2つは、14歳の誕生日の前後の日で計算しています。当然、13歳と14歳として計算されなければ
なりません。このケースではROUND関数も失格になってしまいました (逆にINT/ROUNDDOWNが上
手くいっているようにみえます)。
このように、丸め関数を併用したとしても、扱う日付によって上手くいく場合と駄目な場合があります。つ
まり、丸め関数を併用したとしても YEARFRAC 関数では 【 常に正しい年齢計算 】 は出来ません。
何故出来ないのかは、この上で解説している YEARFRAC 関数の計算方式を読んでください (そもそも、
暦に従った計算ではなく、Excel 独自の怪しい計算なのですから、正しい結果など望むべくもありま
せん) 。
キーワード 「YEARFRAC 年齢」 で検索すると、この使い方が如何に氾濫しているかが判ると思います。
これは MS 自体が間違った例を紹介しているというのも影響しているのでしょう。
Microsoft Office Excel ヘルプセンター [ VLOOKUP / 使用例 4 ] INT との併用で年齢計算 (基準:1)
もう少し、サンプル を提示すると ・・・
2004/1/1 〜 2017/1/1 YEARFRAC⇒ 13.00078217 ROWNDDOWN⇒13歳 〇
2004/3/1 〜 2017/3/1 YEARFRAC⇒ 12.99804458 ROWNDDOWN⇒12歳 ×(満13歳)
2005/3/1 〜 2018/3/1 YEARFRAC⇒ 13.00058674 ROWNDDOWN⇒13歳 〇
2006/3/1 〜 2019/3/1 YEARFRAC⇒ 13.00058674 ROWNDDOWN⇒13歳 〇
2007/3/1 〜 2020/3/1 YEARFRAC⇒ 13.00078217 ROWNDDOWN⇒13歳 〇
2008/3/1 〜 2021/3/1 YEARFRAC⇒ 12.99804458 ROWNDDOWN⇒12歳 ×(満13歳)
2008/3/1 〜 2024/3/1 YEARFRAC⇒ 15.99806763 ROWNDDOWN⇒15歳 ×(満16歳)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[ この場所へのリンク ]
『OOo.Calc YEARFRAC 基準:1 (Actual/Actual)』 について
参考 : OpenOffice.org Wiki ( YEARFRAC , Financial date systems )
この調査は [ Open Office.org 2.4.0 および 3.0.0 ( 現 Apache OpenOffice ) ] で行なっています。
OOo.Calc の YEARFRAC (Basis : 1, Act/Act) についても算出結果を分析したところ、Excel のものとは
異なり、下記の算出方法を採っているようです。
a) [ 開始日〜終了日] から [ y 年 と d 日間 ] という期間を算出する (The remainning final stub period )。
b) 年比率 = y + ( d ÷ 年間日数 [ 365 or 366 ] ) で比率を計算する。
c) 365 と 366 の何れで割るかは、開始日が属する年の年間日数とする。
(開始日=12/31 の場合でも [ 開始日の年] の年間日数で割る)。
これに ピッタリ と符合する規定は見当たりません。OASIS ODF 仕様 ではYEARFRAC の項にAct/Act は
AFB 方式 と記載されています。その AFB 仕様を見てみると確かに近いようですが、期間(年数)を数える
「向き」 と、365/366の何れで割るかの判断方法が上記の調査結果と異なります。ところで、c) のカッコ内
の条件は、初日不算入の前提から考えれば 「翌年の年間日数」 とするべきところではないかと思えます
(開始年の日は全く期間に加わっていませんから)。
なお、上記 a) 〜 c) の仕様だけでは解決しない微妙なズレがありました。そこで、ktDATEDIF [YD] による
下記6パターンの計算結果を用意して、これと YEARFRAC の結果を比較したところ、[ d日間] の算出過程で
[ ±1日増減 ] している事が判りました。
=YEARFRAC (A1;B1;1)
=ktDATEDIF (A1;B1;"y") + (ktDATEDIF (A1;B1;"yd") / 365)
=ktDATEDIF (A1;B1;"y") + ((ktDATEDIF (A1;B1;"yd") - 1) / 365)
=ktDATEDIF (A1;B1;"y") + ((ktDATEDIF (A1;B1;"yd") + 1) / 365)
=ktDATEDIF (A1;B1;"y") + (ktDATEDIF (A1;B1;"yd") / 366)
=ktDATEDIF (A1;B1;"y") + ((ktDATEDIF (A1;B1;"yd") - 1) / 366)
=ktDATEDIF (A1;B1;"y") + ((ktDATEDIF (A1;B1;"yd") + 1) / 366)
2007/2/28 〜 2008/2/29 …… 「1年1日」 ( ktDATEDIF では 「1年0日」 )
2004/2/29 〜 2005/2/28 …… 「0年365日」 ( 〃 「1年0日」 )
2006/3/31 〜 2008/3/30 …… 「1年364日」 ( 〃 「1年365日」 )
2004/3/31 〜 2006/3/30 …… 「1年365日」 ( 〃 「1年364日」 ) 等
増減パターンを整理すると以下のようになっています( M1D1, M2D2 は各々 開始日/終了日の [月日] )。
(1) [ 2月末日〜2月末日] の場合、平年/閏年に関わらず 「N 年0日」 と算出するべきところを、±1日
増減している( 2007/2/28→2008/2/29 が 「1年1日」 、2004/2/29→2005/2/28 が 「0年365日」 、
初日不算入の計算ならば、この2つは[ 3/1からカウントして2月末日で満1年] なので「1年0日」 )。
(2) 平年→閏年で、M1D1>M2D2 の場合、d が1日少ない ( M1D1 > 2/28 )。
ただし、終了年が 開始年の翌年(このケースでは期間が 1年以下 になる)では正しい。
(3) 閏年→平年で、M1D1>M2D2 の場合、d が1日多い。
ただし、終了年が [ 閏年( 開始年以外でも) の翌年 ] では正しい。
(4) 閏年→閏年で、M1D1>M2D2 の場合、dが1日多い (M1D1 < 2/29)。(終了年の前年は[平年])
(5) 平年→平年で、M1D1>M2D2 の場合、dが1日少ない (M1D1<2/28)。
ただし、終了年が [ 閏年の翌年 ] 以外では正しい。
補足: (2) の状況は この ISSUE (バグ情報) でも報告されてます。
上記のパターンを満たす計算方式を検討した結果、以下のロジックと同等のものであろうと推察されます。
この計算は、下図の二重線部分(端数期間) の期間日数を下記手順で行なっています。
------------(丸y年)--------><----(端数d日間)---->
-#-------------------#--------*==========#=========*---------#-- ( # : 1月1日 )
<--------[a]-------><---[c]--->
<--[b]-->
( i ) 日数 = [c] + ( [a] - [b] ) [a] [b] [c] は1月1日からの年内通算日(1〜366)
( ii ) [a] には、[a] 本来の年間日数ではなく、開始日の年の年間日数(365 or 366) を採用する。
( iii ) 上図で、[b] は本来、終了日[c]の前年であるが、何故か、終了日の年の中で算出する。
上記 (ii) (iii) によって、2/29 の [1日分] が 余計に加算/減算されるという結果になります。
【 OOo.Calc YEARFRAC (基準:1 Actual/Actual ) の 実装ロジック 】 [ シミュレーション ]
Public Function ooo_YEARFRAC(ByVal StartDate As Date, ByVal EndDate As Date) As Double
Dim intYears As Integer
Dim intDays As Integer
Dim SDate As Integer
Dim EDate As Integer
'年内の通算日(端数の日数計算用)、開始[日]も終了年の中に設定する
SDate = DatePart("y", DateSerial(Year(EndDate), Month(StartDate), Day(StartDate)))
EDate = DatePart("y", EndDate)
If (Format(StartDate, "mmdd") <= Format(EndDate, "mmdd")) Then
intYears = Year(EndDate) - Year(StartDate)
'(1/1〜終了日の通算日数) - (1/1〜開始[日]の通算日数)
intDays = EDate - SDate
Else
If (Year(StartDate) + 1 = Year(EndDate)) Then
'終了日が開始日の翌年(開始月日>終了月日なので1年未満の期間)
intYears = 0
intDays = EndDate - StartDate
Else
'1年以上の期間
intYears = Year(EndDate) - Year(StartDate) - 1
'(開始[日]〜12/31までの日数) + (1/1〜終了の通算日数)
'年間日数は開始日の[年]を適用する
If (Month(DateSerial(Year(StartDate), 2, 29)) = 2) Then
intDays = (366 - SDate) + EDate
Else
intDays = (365 - SDate) + EDate
End If
End If
End If
'端数期間の年間日数は開始日の[年]を適用する
If (Month(DateSerial(Year(StartDate), 2, 29)) = 2) Then
ooo_YEARFRAC = intYears + CDbl(intDays) / 366#
Else
ooo_YEARFRAC = intYears + CDbl(intDays) / 365#
End If
End Function
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OOo.Calc はオープンソフトなので、本物の ソースコード(GetYearFrac & GetDiffParam) が閲覧で
きました。その ソースコード をVBA風に編集(計算手順はそのまま)したものが下記です。上記の
想定コードとは異なりますが、行なっている計算の意味合いは同じです。何故結果が間違ってしまう
のかを理解するには上記の想定コードの方が視覚的で判り易いでしょう。
【 OOo.Calc の 本物のコード (VBA風アレンジ版) 】
Public Function OOo_YEARFRAC(ByVal StartDate As Date, ByVal EndDate As Date) As Double
Dim intDaysInYear As Integer
Dim intYears As Integer
Dim intDays As Integer
If (Month(DateSerial(Year(StartDate), 2, 29)) = 2) Then
intDaysInYear = 366
Else
intDaysInYear = 365
End If
If (Format(StartDate, "mmdd") <= Format(EndDate, "mmdd")) Then
intYears = Year(EndDate) - Year(StartDate)
Else
intYears = Year(EndDate) - Year(StartDate) - 1
End If
If (intYears = 0) Then
intDays = EndDate - StartDate
Else
intDays = EndDate - DateSerial(Year(EndDate), Month(StartDate), Day(StartDate))
If (intDays < 0) Then
intDays = intDays + intDaysInYear
End If
End If
OOo_YEARFRAC = intYears + CDbl(intDays) / CDbl(intDaysInYear)
End Function
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[ この場所へのリンク ]
---- Act/Act (AFB) ( or Act/365L ) の計算方式について ---
下記の解説を読むと、Act/Act (AFB) は
「終了日から遡って 1年間・2年間 と数え、最後に残った"開始日から始まる端数期間"
部分を、 365 or 366 で割る ( その端数期間内に2月29日が有る/無しに拠る ) 。
なお、2/28 の1年前は、その年が 閏年なら 2/29、平年なら 2/28 とします。 」
という仕様のようです( The remaining initial stub period ) 。これは、期間を数える向きを逆にした
『具体的 2月29日説』 といえるでしょう。
AFB : Association Francaise des. Banques , フランス銀行協会
ISDA ドキュメント - Page 3 -- Actual/Actual ( AFB )
Under the AFB method, the denominator is either 365 (if the calculation period does not contain
29th February) or 366 (if the calculation period includes 29th February) - where a period of longer
than one year is involved, two or more calculations are made: interest is calculated for each full
year, counting backwards from the end of the calculation period, and the remaining initial stub
period is treated in accordance with the usual rule.When counting backwards for this purpose, if
the last day of the relevant period is 28th February, the full year should be counted back to the
previous 28th February unless 29th February exists, in which case, 29th February should be used.
【 正規 Actual/Actual (AFB) の ロジック 】
Public Function AFB_YEARFRAC(ByVal StartDate As Date, ByVal EndDate As Date) As Double
Dim wkDate As Date '応当日
Dim dblFracDays As Double
Dim intYears As Integer
Dim intDays As Integer
Dim LeapDay As Date
If (Format(EndDate + 1, "mmdd") = "0301") Then
'終了日=2月末日
If (Format(StartDate + 1, "mmdd") = "0301") Then
'2月末日〜2月末日
intYears = Year(EndDate) - Year(StartDate)
intDays = 0
ElseIf (Format((StartDate), "mmdd") <= Format(EndDate, "mmdd")) Then
' 1/15〜2月末日 など
If (Year(StartDate) = Year(EndDate)) Then
'同一年内
wkDate = EndDate
intYears = 0
intDays = (wkDate - StartDate)
Else
'終了日(2月末日)→開始年の2月末日でN年
wkDate = DateSerial(Year(StartDate), 3, 1) - 1
intYears = Year(EndDate) - Year(wkDate)
intDays = wkDate - StartDate
End If
Else
' 3/15〜2月末日 など
'終了日(2月末日)→開始日の翌年の2月末日でN年
wkDate = DateSerial(Year(StartDate) + 1, 3, 1) - 1
intYears = Year(EndDate) - Year(wkDate)
intDays = wkDate - StartDate
End If
Else
'終了日≠2月末日
If (Format((StartDate), "mmdd") <= Format(EndDate, "mmdd")) Then
If (Year(StartDate) = Year(EndDate)) Then
'同一年内
wkDate = EndDate
intYears = 0
intDays = wkDate - StartDate
Else
'終了日→開始年の応当日でN年
wkDate = DateSerial(Year(StartDate), Month(EndDate), Day(EndDate))
intYears = Year(EndDate) - Year(wkDate)
intDays = wkDate - StartDate
End If
Else
'終了日→開始年の翌年の応当日でN年
wkDate = DateSerial(Year(StartDate) + 1, Month(EndDate), Day(EndDate))
intYears = Year(EndDate) - Year(wkDate)
intDays = wkDate - StartDate
End If
End If
If (Month(DateSerial(Year(StartDate), 2, 29)) = 2) Then
LeapDay = DateSerial(Year(StartDate), 2, 29)
ElseIf (Month(DateSerial(Year(wkDate), 2, 29)) = 2) Then
LeapDay = DateSerial(Year(wkDate), 2, 29)
Else
LeapDay = 0
End If
If (LeapDay = 0) Then
dblFracDays = 365#
Else
If (StartDate < LeapDay) And (LeapDay <= wkDate) Then
dblFracDays = 366#
Else
dblFracDays = 365#
End If
End If
AFB_YEARFRAC = intYears + (CDbl(intDays) / dblFracDays)
End Function
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[ この場所へのリンク ]
『Google SpreadSheet YEARFRAC 基準:1 (Actual/Actual)』 について
Google SpreadSheet の YEARFRAC 関数についても調査したところ、Google SpreadSheet では [基準:1,Act/Act]
の計算方法は ISDA 方式で行なっている事が判明しました。
ISDA 方式 : 期間を 1年間(1月1日〜12月31日)ごとに区切り、各々の年について、
その部分での期間日数を 「その年の年間日数(365 or 366)」で割り、
その結果を合計する。
【 Google SpreadSheet YEARFRAC (基準:1 Actual/Actual ) の 実装ロジック 】 [ シミュレーション ]
Public Function Google_YEARFRAC(ByVal StartDate As Date, ByVal EndDate As Date) As Double
Dim i As Integer
Dim dblFrac As Double
Dim SDate As Date
Dim EDate As Date
dblFrac = 0
For i = Year(StartDate) To Year(EndDate)
SDate = DateSerial(i, 1, 1)
EDate = DateSerial(i + 1, 1, 1)
If (SDate < StartDate) Then
SDate = StartDate
End If
If (EDate > EndDate) Then
EDate = EndDate
End If
If (Month(DateSerial(i, 2, 29)) = 2) Then
dblFrac = dblFrac + (CDbl(EDate - SDate) / 366#)
Else
dblFrac = dblFrac + (CDbl(EDate - SDate) / 365#)
End If
Next i
Google_YEARFRAC = dblFrac
End Function
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[ この場所へのリンク ]
ktDATEDIF , DATEDIF(Excel2003以前) , DATEDIF(Excel2007 SP無,SP1 ) での算出結果の違い
以前 から有名だった
1998/10/31〜2000/3/21 MD: 19 (誤 Excel2003) , 21 (正 Excel2007)
1998/10/31〜2000/3/1 MD: -1 (誤 Excel2003) , 1 (正 Excel2007)
という ケースの計算が直った反面で、
2008/2/6〜2008/3/3 MD: 26 (正 Excel2003) , 28 (誤 Excel2007)
2006/12/31〜2007/2/1 MD: 1 (正 Excel2003) , -2 (誤 Excel2007)
といったケースでは、2003以前では正しかったのが、2007では間違った結果を返すようになっています。
2007での修正によって却って誤計算のパターンが増えたように思えます。
YD にいたっては、30〜300日も少なく計算されるケースも出て来ました。
2001/3/1〜2004/1/31 YD : 336 (正 Excel2002) , 336 (正 Excel2007)
2001/3/1〜2004/2/1 YD : 337 (正 Excel2002) , 305 (誤 Excel2007)
2001/12/1〜2003/11/1 YD : 335 (正 Excel2002) , 21 (誤 Excel2007)
( Y , YM , MD ) ktDATEDIF Excel2003 Excel2007
2008/ 3/31 - 2009/ 2/28 0年11ヶ月 0日 0年10ヶ月28日 0年10ヶ月25日
2008/ 4/30 - 2009/10/31 1年 6ヶ月 0日 1年 6ヶ月 1日 1年 6ヶ月 1日
2008/ 4/30 - 2009/ 3/ 1 0年10ヶ月 1日 0年10ヶ月-1日 0年10ヶ月 2日
2008/ 5/31 - 2009/ 3/ 1 0年 9ヶ月 1日 0年 9ヶ月-2日 0年 9ヶ月 1日
2008/ 2/29 - 2009/ 2/28 1年 0ヶ月 0日 0年11ヶ月30日 0年11ヶ月27日
2008/ 2/ 6 - 2012/ 3/ 3 4年 0ヶ月26日 4年 0ヶ月26日 4年 0ヶ月28日
2006/12/31 - 2007/ 2/ 1 0年 1ヶ月 1日 0年 1ヶ月 1日 0年 1ヶ月-2日
( YD ) ktDATEDIF Excel2003 Excel2007
2001/ 1/1 - 2004/ 2/28 58 58 58 (平年→閏年) 59の重複
2001/ 1/1 - 2004/ 2/29 59 [59] [59]
2001/ 1/1 - 2004/ 3/ 1 60 [59] [59]
2001/ 1/1 - 2004/ 3/ 2 61 60 60
2000/ 1/1 - 2005/ 2/28 58 58 58 (閏年→平年) 59の抜け
2000/ 1/1 - 2005/ 3/ 1 59 60 60
2000/ 1/1 - 2005/ 3/ 2 60 61 61
2001/ 3/1 - 2004/ 1/31 336 336 336 Excel2007 の異様な計算結果
2001/ 3/1 - 2004/ 2/ 1 337 337 305
2001/12/1 - 2003/11/ 1 335 335 21
2001/ 5/1 - 2003/ 1/ 1 245 245 245
2001/ 5/1 - 2003/ 1/31 275 275 275
2001/ 5/1 - 2003/ 2/ 1 276 276 244
2001/ 5/1 - 2003/ 2/28 303 303 271
2001/ 5/1 - 2003/ 3/ 1 304 304 243
2001/ 5/1 - 2003/ 3/31 334 334 273
2001/ 5/1 - 2003/ 4/ 1 335 335 242
2001/ 5/1 - 2003/ 4/30 364 364 271
2001/ 5/1 - 2003/ 5/ 1 0 0 0
2001/ 5/1 - 2003/ 5/31 30 30 30
2001/ 5/1 - 2003/ 6/ 1 31 31 31
2001/ 5/1 - 2003/ 6/30 60 60 60
2001/ 5/1 - 2003/ 7/ 1 61 61 61
2001/ 5/1 - 2003/ 7/31 91 91 91
2001/ 5/1 - 2003/12/ 1 214 214 214
2001/ 5/1 - 2003/12/31 244 244 244
Excel2007(YD) の異様な計算結果 (その2)
[ この場所へのリンク ]
2008/3/25: MS はExcel2007 の DATEDIF に不具合があると正式に認めました。
(従来は非公開関数を理由に不具合の有無自体への言及を避けていました)
※ Excel2007(SP無/SP1)が行なっている実装ロジックを解明しました。 ( 2008/12/13 追記 )
Excel2007(SP無/SP1) DATEDIF(YD) ロジック ( マクロブック : XL2007_DATEDIF_YD.xlsm )
〃 〃 (MD) 〃 ( マクロブック : XL2007_DATEDIF_MD.xlsm )
2009/7/ 9: Excel2007 SP2 で再検証を行なったところ、[ SP2 & その後のアップデート ] で修正が行なわれた
ようです( SP2 [ 12.0.6425.1000 ] , KB 969682 (KB 968863, KB 969952 [ 12.0.6504.5001 ] )。
MD の計算は [2003 以前] に戻りました。YD の計算は 9割方 [2003 以前] に戻りました。
YD の戻らなかった部分( 開始日が2日以降 & 終了日が閏年の1月 ) が KB 969682 で
再度修正 [12.0.6504.5001] されますが、それでも 直っていません。
例: 2000/5/5〜2004/1/4 → 357日 ( 正解: 244日 [ 算出値 = 正解 + 113 ] )
不具合発生条件: 終了年月日が 『 年= 閏年 , 月= 1月 , 日< 開始年月日の[日] 』
尚、SP1 でも、最新の修正プログラム( KB968863, KB969952 )まで更新を行なえば、この修正が掛ります。
2010/2/26 : Excel 2010β で確認したところ、上記 Excel2007(SP2 + KB 969682) [12.0.6504.5001] と同じです。
2010/5/28 : Excel2010(RTM) で確認したところ、残っていた[YD]の一部不一致も修正され Excel2003以前に完全
に戻りました (バグが完全に無くなったという意味ではありません。Excel2003以前と同じバグはあります)。
[ この場所へのリンク ]
2011/4/ 9 : Excel2007SP2 の修正により
『[YD の一部] を残して2003以前に戻った ( MD は完全に元に戻った ) 』
とアナウンス(2009/7/9)していましたが、[MD の一部] にも修正漏れが残っている事が判りました。
例: 2011/3/20 - 2012/1/19 → 143日 ( 正解: 30日 [ 算出値 = 正解 + 113 ] )
不具合発生条件: 終了年月日が 『 年= 閏年 , 月= 1月 , 日< 開始年月日の[日] 』
尚、Excel2010 では、この部分も修正されており 2003以前と同じ結果が得られます。
2011/11/10 : Excel2007SP3 の修正でも、上記 「 MD の修正漏れ」 は修正されていません。
2012/2/20 : Excel2007(SP3) の DATEDIF 不具合に関するサポート技術情報が公開されました ( KB2678753 )
[ この場所へのリンク ]
2014/2/12 : MD および YD ともに 下記条件において [ 算出値 = 正解 + 113 ] となる不具合が残っていましたが、
KB2827324 ( 2013/10/8 リリース ) によって、この [ + 113 ] が [ + 164 ] に変わっている
事が判明しました。(MD/YD とも。発生条件は変わりなし)
不具合発生条件: 終了年月日が 『 年= 閏年 , 月= 1月 , 日< 開始年月日の[日] 』
本件の詳細と回避策などについては下記の赤枠内の解説を参照して下さい。
2014/3/7 : 上記、不具合の更なる デグレード に対して、2012/2/20 にリリース されていた MS-KB KB2678753
が 下記 赤枠内の回避策を基にして 解説が修正されました。
[ この場所へのリンク ]
上記の不具合( 2014/2/12 ) については、Microsoft テクニカルサポートに調査をして貰ったところ、修正アップデート
KB2827324 の適用によって発現している事が判明しました。なお、 この妙な誤差は過去の修正アップデートで以下
のような経歴を持っています。
・ SP2 (12.0.6425.1000) ⇒ [ + 164 ]
・ SP2 + KB969682 (12.0.6504.5001) ⇒ [ + 113 ]
・ SP3 (SP2 + KB969682 が集約 , 12.0.6611.1000) ⇒ [ + 113 ]
・ SP3 + KB2760583 (12.0.6679.5000) ⇒ [ + 113 ] 2013/9/10 リリース
・ SP3 + KB2827324 (12.0.6683.5002) ⇒ [ + 164 ] 2013/10/8 リリース
私的な推測ですが、
この変更は DATEDIF に対しての修正(Issue)ではなく、他の障害への修正が 『DATEDIF も利用
している日付関連の共通関数』 に及び、それに伴って DATEDIF の算出結果にも影響が出た
のではないかと想像しています。そうだとすると、今後もアップデートの度に繰り返し値が変わる可能性があります。
その点も考慮すると、KB2678753 (2014/3/7 本枠内の回避策で修正されています) で紹介している補正式を
単純に 式内の [ 113 ] を [ 164 ] へ修正する
という 解決策 では不完全です。
あまり良い方法ともいえませんし、更に長ったらしいものになりますが、
(1) そのPC上の Excel2007 で現われる誤差を求める
(2) その求めた誤差を補正式内の [ 113 ] の位置に当て嵌める
とすれば、アップデート状況に依らず対応できます。 2011/1/2 〜 2012/1/1 の期間を計算した場合、
[ YD ] 正解 = 364 , [ MD ] 正解 = 30
ですので、
[ YD ] DATEDIF(DATE(2011,1,2), DATE(2012,1,1), "YD") - 364
[ MD ] DATEDIF(DATE(2011,1,2), DATE(2012,1,1), "MD") - 30
という式で 113 や 164 といった補正値を得る事ができます。
-- YD --
( 2012/2/20 時点での KB2678753 の回避式 )
=IF(AND(INFO("release")="12.0",MONTH(DATE(YEAR(B1),2,29))=2,MONTH(B1)=1,DAY(A1)>DAY(B1)),
DATEDIF(A1,B1,"YD") - 113,
DATEDIF(A1,B1,"YD"))
↓
=IF(AND(INFO("release")="12.0",MONTH(DATE(YEAR(B1),2,29))=2,MONTH(B1)=1,DAY(A1)>DAY(B1)),
DATEDIF(A1,B1,"YD") - (DATEDIF(DATE(2011,1,2), DATE(2012,1,1), "YD") - 364),
DATEDIF(A1,B1,"YD"))
-- MD --
( 2012/2/20 時点での KB2678753 の回避式 )
=IF(AND(INFO("release")="12.0",MONTH(DATE(YEAR(B1),2,29))=2,MONTH(B1)=1,DAY(A1)>DAY(B1)),
DATEDIF(A1,B1,"MD") - 113,
DATEDIF(A1,B1,"MD"))
↓
=IF(AND(INFO("release")="12.0",MONTH(DATE(YEAR(B1),2,29))=2,MONTH(B1)=1,DAY(A1)>DAY(B1)),
DATEDIF(A1,B1,"MD") - (DATEDIF(DATE(2011,1,2), DATE(2012,1,1), "MD") - 30),
DATEDIF(A1,B1,"MD"))
尚、この方法を採る場合、Excel2007 以外では誤差値が ゼロ になるので、Excel2007 である判定式
『 INFO("release")="12.0" 』
が無くても支障ありません。支障ありませんが、こんな意味不明な式を使う説明(Excel2007限定)にもなりますので
残しておいた方が良いと思います。
また、後々、理由を知らない人が [ 2011/1/2 , 2012/1/1 ] の日付を弄ってしまう危険性もあります。そこで、この
危険を避ける為に 補正値の式の部分を【名前定義】で登録 しておきます。
-- 名前定義の方法 --
[ 数式タブ/名前の定義 ]
名前 : Excel2007YD補正値
範囲 : ブック
コメント : Excel2007/DATEDIF(YD)のバグによる誤差を補正する
参照範囲 : =DATEDIF(DATE(2011,1,2), DATE(2012,1,1), "YD") - 364
名前 : Excel2007MD補正値
範囲 : ブック
コメント : Excel2007/DATEDIF(MD)のバグによる誤差を補正する
参照範囲 : =DATEDIF(DATE(2011,1,2), DATE(2012,1,1), "MD") - 30
そうすると、下記式になります。
-- YD --
=IF(AND(INFO("release")="12.0",MONTH(DATE(YEAR(B1),2,29))=2,MONTH(B1)=1,DAY(A1)>DAY(B1)),
DATEDIF(A1,B1,"YD") - Excel2007YD補正値,
DATEDIF(A1,B1,"YD"))
-- MD --
=IF(AND(INFO("release")="12.0",MONTH(DATE(YEAR(B1),2,29))=2,MONTH(B1)=1,DAY(A1)>DAY(B1)),
DATEDIF(A1,B1,"MD") - Excel2007MD補正値,
DATEDIF(A1,B1,"MD"))
|
||
角田 桂一 Mail:addinbox@h4.dion.ne.jp CopyRight(C) 2001 Allrights Reserved. |