none
経過月数の計算について RRS feed

  • 質問

  • こんばんは。

    VB2010+SQLServer2008を使用しております。

    経過月数を計算する方法を考えております。

    単純にDateDiffを使用すると同月の場合、「0」となってしまうので、まずは下記のように記述をしました。

     Dim cnt As Integer = DateDiff(DateInterval.Month, Startdate, EndDate)
     Dim D1 As Date = DateAdd(DateInterval.Day, -1, _
                                     DateAdd(DateInterval.Month, cnt + 1, Startdate))

            If D1 = EndDate Then
                cnt += 1
            End If

    しかし、この場合だと、1/31~2/28は「1」を返してくれるのですが、
    1/31~2/27の場合でも「1」を返してしまいます。
    (この場合は、「0」を返してほしい)

    これ以降の条件をどのように記述をすればよいか、悩んでおります。

    いくつか条件を加えてテストをしておりますが、どうしてもこれといったものが見つかりません。

    どうか、アドバイスをお願いします。


    • 編集済み TI-cb400 2014年1月14日 9:13
    2014年1月14日 9:11

回答

  • trapemiya さま 横から失礼します。

     私の認識では「0」になると理解していたのですが、この場合、なぜ「1」になるのでしょうか? に対しての投稿です。

    質問者さんは 開始日と終了日を含む 1ヶ月を想定しているのだと思います。
    経過日数ではなく、期間で捉えられているのでしょう。

    私の解釈は、翌月前日になった時 が 1ヶ月と (勝手に) 判断しました。

       その前提で以前の投稿をしています。 3/27~4/26 も ok でした。

    質問者さんが、明確に、1ヶ月の定義をして下されば、トラブル少ないんですけどね。

    • 編集済み ShiroYuki_Mot 2014年1月16日 3:02 改行崩れを訂正
    • 回答としてマーク TI-cb400 2014年1月16日 11:44
    2014年1月16日 2:59
  • 末日が絡むというか、起算した結果が存在しない日の場合は末日に置き換えるという事でしょう。

    やはり、末尾に置き換えるということですね。私もそう思っていました。

    因みに、「オブジェクトブラウザ」に表示される System.Date メンバ AddMonths の処理内容と同一と言う事だと思います。

    これは知りませんでした。考えてみればなるほどの仕様ですが、勉強になりました。ありがとうございます。
    以上を踏まえた上で、新たにコードを考えてみました。単体テストでは、上記に出た日付のテストは全てクリアしています。

    '経過月数を求める。
    Private Function Get経過月数(stdt As Date, eddt As Date) As Integer
    
        '開始日から1か月単位の先の日で、かつ、終了日以上となる日を満期日と定義する。
        'Dim 満期日 As Date
    
        'Dim result As Date
    
        '満期日があり得ない日付(2月30日とか)になる場合は、満期日の月末を満期日とする。
        'If (Date.TryParse(eddt.Year.ToString() & "-" & eddt.Month.ToString() & "-" & stdt.Day.ToString(), result)) Then
        '    満期日 = New Date(eddt.Year, eddt.Month, stdt.Day)
        'Else
        '    満期日 = New Date(eddt.Year, eddt.Month, 1).AddMonths(1).AddDays(-1)
        'End If
    
        '満期日があり得ない日付(2月30日とか)になる場合は、満期日の月末を満期日とする。
        Dim 満期日 = stdt.AddMonths((eddt.Year * 12 + eddt.Month) - (stdt.Year * 12 + stdt.Month))
    
        '満期日よりも終了日が大きい場合は、少なくとも満期日は終了日より1か月先にある。
        If (満期日 < eddt) Then
            満期日 = 満期日.AddMonths(1)
        End If
    
        Dim 満期日前日 = 満期日.AddDays(-1)
    
        Dim 経過月数 = DateDiff(DateInterval.Month, stdt, 満期日)
    
        Return IIf(eddt < 満期日前日, 経過月数 - 1, 経過月数)
    
    End Function
    #(コード修正)Get経過月数の引数の型をDateTimeからDateに変更

    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/


    2014年1月16日 7:33
    モデレータ
  • >自分が行う条件で反問題がなかったのですが、問題点などがありましたら、ご指摘いただけると
    幸いです。

    最後の、cnt -= 1 の部分ですが、これだと例えば、 1/1から2/5のようにStartDateが1日である場合に問題が出ませんでしょうか?


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    • 回答としてマーク TI-cb400 2014年1月17日 9:00
    2014年1月17日 2:15
    モデレータ
  • TI-cb400 さま 何かありましたらにお答えします。

    以下に書く事は、自分への戒めも込めて書く一般的な注意事項です。

    今回のご質問のコードもご自分で導き出されたコードも、
    算出したデータに対して、補正を行うものですよね。
    ご質問に至ったのは、この例外処理に行き詰ったからだと思います。

      あるいは、後日、想定していなかった値を放り込まれて、とんでもない値を吐く事もありますね。

    デバッグして、期待値を求められない場合、この様に、例外処理を書き加える事は良くあります。
    しかし、人間のやる事です。
    落ち漏れや、後日の変更時に分かり難いコードになってしまう事が、多々あります。
    If 文のオンパレードは頭が混乱しやすいです。 漏れが出易いものです。

    可能であれば、考え方の方向転換や処理方法の変更をも考慮して下さい。
    可能な限り、条件文で分岐させるのは最小限にする方が、後々のメンテも楽になりますから。

    以上、読み飛ばして下さい。 お疲れ様でした。

    • 回答としてマーク TI-cb400 2014年1月17日 9:00
    2014年1月17日 3:53

すべての返信

  • 「経過月数」の「定義」を「文章」で書きましょう。
    それが無いとだれも回答できないと考えられます。

    とは言うものの、文面から察するに。

    仮定1.求める時刻範囲である「経過月数」には、開始と終了の位置が含まれる。

    ように読めます。その仮定を採用すると、

    1.日とはその0:00~23:59:59の範囲を意味する(定義)。
    2.従って、終了日を含める場合は、その日の時刻に関わらず
     終了日の23:59:59を超えていると判定しなければならない。

    従って、本件の場合

    3.DateDiff()で判定すべき範囲は、
     「開始日」から「終了日の翌日」までの月数である。

    と考えられます。間違ってたらすみません。
    ただし、DateDiff()の仕様を良く知りませんので、
    年をまたぐ場合や、閏年の場合などにどうなるかわかりません。

    2014年1月14日 9:58
  • 開始日からNヶ月後の日にちと比較すればいいんじゃないかなぁ

    Private Function MonthDiff(ByVal da As DateTime, ByVal db As DateTime) As Integer
    
        If (da > db) Then
            Dim d As DateTime = da
            da = db
            db = d
        End If
    
        '年月のみの経過月数Nを計算
        Dim N_month As Integer = (db.Year - da.Year) * 12 + db.Month - da.Month
        '開始日からNか月後の日にちを計算
        Dim dbTemp As DateTime = da.AddMonths(N_month)
    
        If (db < dbTemp) Then
            '終了日が開始日のNか月後よりも前なら最後の月は満了していない
            Return N_month - 1
        Else
            Return N_month
        End If
    
    End Function
    #SQL Serverは関係ない質問な気がする

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2014年1月14日 9:59
  • 仲澤@失業者さんが書かれているように、経過月数の定義がはっきりわからないのですが、開始日と終了日の間に丸ごと含まれる月がいくつあるかということであれば、例えば以下のように判断できると思います。

    '経過月数を求める。
    Private Function Get経過月数(stdt As DateTime, eddt As DateTime) As Integer
    
        Return eddt.Month - stdt.Month - IIf(Is月末日(eddt), 0, 1)
    
    End Function
    
    '月末日かどうかを求める。
    Private Function Is月末日(dt As DateTime) As Boolean
    
        Dim dt1 = dt.AddDays(1)
    
        Return dt.Month <> dt1.Month
    
    End Function
    #(追記)あ~、すみません。掲載されたコードを見ると、こういう意味じゃなさそうですね。日も加味する必要がありそうですね。今、時間が無いので投げっぱなしですみません。

    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/


    2014年1月15日 0:38
    モデレータ
  • ご回答有難うございます。

    肝心なことを書かず、大変申し訳ありません。

    日も加味してNヶ月経過したかどうかの判定をしたいと考えています。
    また、月初から月末までの場合も「1月」とカウントしたいと考えています。

    例として

    1/1~1/31  1ヵ月
    1/1~2/28  2ヶ月
    1/30~2/28 1ヵ月
    1/25~2/27 1ヵ月
    1/24~2/15 0ヶ月
    となるようにしたいと考えています。

    DateDiffの場合、月が異なる場合にはその「1月」と計算をしてくれるようですが、
    1/30~2/28のような場合の計算がうまくいかず、悩んでおります。

    よろしく御願い致します

    2014年1月15日 4:21
  • 日の大小を加味した上で経過月をまず求めますが、終了日が月末の場合に特異な場合が発生します。それは、

    1.終了日が月末日で、かつ開始日が1日の場合
    2.終了日が月末日で、かつ、開始日の日より終了日の日が小さい場合

    の2つだと思います。これらの場合には経過月を1つ増やさなければなりません。
    以上、コードにすると以下になります。

    '経過月数を求める。
    Private Function Get経過月数(stdt As DateTime, eddt As DateTime) As Integer
    
        Dim 経過月数 As Integer
    
        経過月数 = eddt.Month - stdt.Month - IIf(stdt.Day <= eddt.Day, 0, 1)
    
        If (Is月末日(eddt) And stdt.Day = 1) Or
           (Is月末日(eddt) And stdt.Day > eddt.Day) Then
    
            経過月数 += 1
        End If
    
        Return 経過月数
    
    End Function
    
    '月末日かどうかを求める。
    Private Function Is月末日(dt As DateTime) As Boolean
    
        Dim dt1 = dt.AddDays(1)
    
        Return dt.Month <> dt1.Month
    
    End Function
    


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2014年1月15日 5:33
    モデレータ
  • TI-cb400 さま よろしく。

    皆様のご指摘の様に、1ヶ月の定義が不明瞭なので、書かれた内容から推測で書いて行きます。
    例として示された内容(7件)は一応クリヤしています。

    考え方として、期間を求める事から、開始時点と終了時点の 前日と翌日を求めます。
    これに対して、開始時点の 翌月同日を算出し、終了時点と比べます。
    これを月単位でループして、ループの回数から月数を求めました。
    他の方と違い、泥臭い方法ですが、如何でしょうか。

        Private Function GetMonthExpiration(ByVal dayFrom As Date, ByVal dayTo As Date) As Integer
            Dim valueMonths As Integer = 0
            Dim dayStartPre As Date = dayFrom.AddDays(-1)
            Dim dayEndNext As Date = dayTo.AddDays(1)
            Dim dayMonthlyTop As Date = dayStartPre
            Dim loopCtrl As Boolean = True

            Do While loopCtrl
                Dim monthlyEnd As Date = dayMonthlyTop.AddMonths(1)

                If monthlyEnd >= dayEndNext Then
                    Exit Do
                Else
                    valueMonths = valueMonths + 1
                    dayMonthlyTop = monthlyEnd
                End If
            Loop

            Return valueMonths
        End Function


    • 編集済み ShiroYuki_Mot 2014年1月15日 11:11 検証件数を追加
    2014年1月15日 11:03
  • 経過月数を求めるところ、年を12倍、かな。 (eddt.Year * 12 + eddt.Month) - (stdt.Year * 12 + stdt.Month)

    Jitta@わんくま同盟

    2014年1月15日 12:11
  • 皆様、ご回答有難うございます。

    trapemiya様のご提案をテストしてみた結果です。

    おおむね問題がなかったのですが、こちらのテストでは、

    3/27~4/26

    の場合は、「1」を返してほしいのですが、「0」を返してしまいます。

    また、

    1/30~2/28

    も「1」を返してほしいところですが、「0」を返してしまいます。。

    ご提示いただいたコードをもとに、自分でも考えているところです。

    よろしく御願い致します

    2014年1月16日 2:16
  • >3/27~4/26
    >の場合は、「1」を返してほしいのですが、「0」を返してしまいます。

    私の認識では「0」になると理解していたのですが、この場合、なぜ「1」になるのでしょうか?

    >1/30~2/28
    >も「1」を返してほしいところですが、「0」を返してしまいます。。

    これは、2014/01/30~2014/02/28 だとすれば、私のコードでは「1」を返すことを確かめましたが、年が違うのでしょうか?
    例えば、うるう年であれば、2/28は月末ではなくなりますので、「0」を返すはずです。


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2014年1月16日 2:34
    モデレータ
  • trapemiya さま 横から失礼します。

     私の認識では「0」になると理解していたのですが、この場合、なぜ「1」になるのでしょうか? に対しての投稿です。

    質問者さんは 開始日と終了日を含む 1ヶ月を想定しているのだと思います。
    経過日数ではなく、期間で捉えられているのでしょう。

    私の解釈は、翌月前日になった時 が 1ヶ月と (勝手に) 判断しました。

       その前提で以前の投稿をしています。 3/27~4/26 も ok でした。

    質問者さんが、明確に、1ヶ月の定義をして下されば、トラブル少ないんですけどね。

    • 編集済み ShiroYuki_Mot 2014年1月16日 3:02 改行崩れを訂正
    • 回答としてマーク TI-cb400 2014年1月16日 11:44
    2014年1月16日 2:59
  • ご回答有難うございます。

    trapemiya様。私の説明が悪く、大変申し訳ありません。

    1ヵ月の定義についてですが、ご提示いただいた表現を用いれば「期間」
    ということになるのでしょうか。

    終了日の日が開始日の日の前日の時点で「1ヵ月」となるように計算をしたいと思っています。

    ShiroYuki_Mot様のコードについてはまだテストができておらず、申し訳ありません。

    今、テストができる環境にありませんので、あとでご報告したいと思います。

    2014年1月16日 3:52
  • TI-cb400 さま 追加情報です。

    検証件数が少ないので、少し、不安です。
    特に、戻値が 3 以上になる場合、期待通りの答えになるか見ていません。「

    基準日を前月から繰り越している部分
     Dim monthlyEnd As Date = dayMonthlyTop.AddMonths(1)
    は一考が必要かも知れません。
     
    dayMonthlyTop.AddMonths(1) ではなく dayStartPre.AddMonths(valueMonths + 1) とか。

    実際に、デバッグなさって問題が生じる場合、ここを弄って下さい。 特に、基準日が 29 日以降の時。
    中途半端なコードで申し訳ありません。

    2014年1月16日 4:30
  • 質問があります。

    >終了日の日が開始日の日の前日の時点で「1ヵ月」となるように計算をしたいと思っています。

    1/30 から 2/26の場合、終了日の前日は2/29にはなりませんが(2014年に2/29は無い)、この場合はどのように判断すれば良いのでしょうjか?


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2014年1月16日 6:29
    モデレータ
  • 経過月数を求めるところ、年を12倍、かな。 (eddt.Year * 12 + eddt.Month) - (stdt.Year * 12 + stdt.Month)
    Jittaさん、ありがとうございます。その通りですね。うかつでした。

    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2014年1月16日 6:30
    モデレータ
  • trapemiya さま 横から失礼します。

    質問者さんから正式にご回答が寄せられるとは思いますが、
    私の返信の語句を流用された感もありますので、コメントを入れます。

    末日が絡むというか、起算した結果が存在しない日の場合は末日に置き換えるという事でしょう。

    因みに、「オブジェクトブラウザ」に表示される System.Date メンバ AddMonths の処理内容と同一と言う事だと思います。
    因みに因みに、ここの説明も、詳しくは書いてありませんね。 サマリーだからでしょうか。


    おふた方へ。
    説明に言葉が足らず、ご迷惑をお掛けしました。

    2014年1月16日 7:17
  • 末日が絡むというか、起算した結果が存在しない日の場合は末日に置き換えるという事でしょう。

    やはり、末尾に置き換えるということですね。私もそう思っていました。

    因みに、「オブジェクトブラウザ」に表示される System.Date メンバ AddMonths の処理内容と同一と言う事だと思います。

    これは知りませんでした。考えてみればなるほどの仕様ですが、勉強になりました。ありがとうございます。
    以上を踏まえた上で、新たにコードを考えてみました。単体テストでは、上記に出た日付のテストは全てクリアしています。

    '経過月数を求める。
    Private Function Get経過月数(stdt As Date, eddt As Date) As Integer
    
        '開始日から1か月単位の先の日で、かつ、終了日以上となる日を満期日と定義する。
        'Dim 満期日 As Date
    
        'Dim result As Date
    
        '満期日があり得ない日付(2月30日とか)になる場合は、満期日の月末を満期日とする。
        'If (Date.TryParse(eddt.Year.ToString() & "-" & eddt.Month.ToString() & "-" & stdt.Day.ToString(), result)) Then
        '    満期日 = New Date(eddt.Year, eddt.Month, stdt.Day)
        'Else
        '    満期日 = New Date(eddt.Year, eddt.Month, 1).AddMonths(1).AddDays(-1)
        'End If
    
        '満期日があり得ない日付(2月30日とか)になる場合は、満期日の月末を満期日とする。
        Dim 満期日 = stdt.AddMonths((eddt.Year * 12 + eddt.Month) - (stdt.Year * 12 + stdt.Month))
    
        '満期日よりも終了日が大きい場合は、少なくとも満期日は終了日より1か月先にある。
        If (満期日 < eddt) Then
            満期日 = 満期日.AddMonths(1)
        End If
    
        Dim 満期日前日 = 満期日.AddDays(-1)
    
        Dim 経過月数 = DateDiff(DateInterval.Month, stdt, 満期日)
    
        Return IIf(eddt < 満期日前日, 経過月数 - 1, 経過月数)
    
    End Function
    #(コード修正)Get経過月数の引数の型をDateTimeからDateに変更

    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/


    2014年1月16日 7:33
    モデレータ
  • 皆様、ご回答有難うございます。

    trapemiya様 ShiroYuki_Mot様よりご提示いただいたコードを試してみました。

    両方とも、自分が考えていることが実現できておりました。

    ありがとうございます。

    ご質問の後、自分なりに考えていたのですが、下記のコードに至りました。

    自分が行う条件で反問題がなかったのですが、問題点などがありましたら、ご指摘いただけると
    幸いです。

    本当に、ご回答有難うございました。

      Dim FirstDate As Date
    
            '開始日の日が終了日の月の月末日より大きい場合は、開始日の日をを終了日の日+1とする
            If Startdate.Day > GetLastDate(EndDate).Day Then
                FirstDate = DateSerial(Startdate.Year, Startdate.Month, EndDate.Day + 1)
            Else
                FirstDate = Startdate
            End If
    
            Dim cnt As Integer = DateDiff(DateInterval.Month, FirstDate, EndDate)
    
            '開始日の日が1日、終了日が月末日の場合
            If FirstDate.Day = 1 AndAlso DateAdd(DateInterval.Day, 1, EndDate).Day = 1 Then
                cnt += 1
            ElseIf cnt > 0 AndAlso FirstDate.AddDays(-1).Day > EndDate.Day Then
                cnt -= 1
            End If
    
            Return cnt
    2014年1月16日 11:50
  • >自分が行う条件で反問題がなかったのですが、問題点などがありましたら、ご指摘いただけると
    幸いです。

    最後の、cnt -= 1 の部分ですが、これだと例えば、 1/1から2/5のようにStartDateが1日である場合に問題が出ませんでしょうか?


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    • 回答としてマーク TI-cb400 2014年1月17日 9:00
    2014年1月17日 2:15
    モデレータ
  • TI-cb400 さま 何かありましたらにお答えします。

    以下に書く事は、自分への戒めも込めて書く一般的な注意事項です。

    今回のご質問のコードもご自分で導き出されたコードも、
    算出したデータに対して、補正を行うものですよね。
    ご質問に至ったのは、この例外処理に行き詰ったからだと思います。

      あるいは、後日、想定していなかった値を放り込まれて、とんでもない値を吐く事もありますね。

    デバッグして、期待値を求められない場合、この様に、例外処理を書き加える事は良くあります。
    しかし、人間のやる事です。
    落ち漏れや、後日の変更時に分かり難いコードになってしまう事が、多々あります。
    If 文のオンパレードは頭が混乱しやすいです。 漏れが出易いものです。

    可能であれば、考え方の方向転換や処理方法の変更をも考慮して下さい。
    可能な限り、条件文で分岐させるのは最小限にする方が、後々のメンテも楽になりますから。

    以上、読み飛ばして下さい。 お疲れ様でした。

    • 回答としてマーク TI-cb400 2014年1月17日 9:00
    2014年1月17日 3:53
  • ご回答有難うございます。

    trapemiya様

    ご指摘のとおりでした。ありがとうございます。

    ShiroYuki_Mot様

    アドバイス有難うございます。

    本当におっしゃる通りで、自分の書いているコードは後から見て理解ができるか正直
    不安なところがあります。

    もっとわかりやすいコードをかけるよう、努力をしていきたいと思います。

    2014年1月17日 9:00