web-dev-qa-db-ja.com

.Years&.MonthsのReal Timespanオブジェクト

次の2つのシナリオを検討してください:シナリオ1)。今日は2012年5月1日、シナリオ2)です。今日は2012年9月1日です。

次に、誰かが残したコメントについて、「このコメントは3か月と12日前に書かれたものです」とWebページに書いたとしましょう。これらの両方のシナリオの日数は、ステートメントがまったく同じであっても常に異なる場合があります。シナリオ1では、「3か月と12日」は102 daysになります。ただし、シナリオ2では、「3か月と12日」は104 daysになります。

さて、私の要点を詳しく説明するために、別の例を使用して、誰かが2013年1月30日にサイトにコメントを残したとします。今日は2013年3月10日です。実際のTimeSpanオブジェクトはこの相対日付を知る必要があり、次のうち:

  • 3月は10日あります、
  • 1月は1日です(30日から31日まで)。
  • 2月は、その日数に関係なく1か月であること(28日であっても)。

したがって、10日+ 1日+ 1か月の合計を意味し、This comment was posted 1 Month and 11 Days agoに変換されます。

これで、MSスタイルのTimeSpanオブジェクト(または任意の言語の任意のTimeSpanオブジェクト)を使用した場合、1月30日から3月10日までの日数(39日)が得られます。これは、TimeSpanオブジェクトが相対日付を格納しないためです。 (TimeSpanを取得するために差し引いた基準日/最初の日付)、それが何ヶ月と何日であるかを尋ねた場合、1か月に30日、または最悪の場合でも30日を超える平均があると仮定します、残りを日数で返すため、39日になると、1か月と9日であることがわかり、This comment was posted 1 Month and 9 Days agoメッセージが表示されます。これらのシナリオは両方とも同じ開始日と現在/終了日を持っていることを覚えておいてください。そうです、Microsoft TimeSpanオブジェクトは、2013年2月の月を考慮する必要があることを通知できないようにすることで、完全に異なるTimeSpanを与えています。丸2日。事実、それは私たちに嘘をつきました。

問題は、人々がこれを信じ、自分の過去の認識がどのように変化するか、過去の認識がどのように変化するか、過去の出来事を自分の心の中で再構築しようとするときの決定と人生の選択を誰が知っているかということです。今日のいたるところに蔓延している時間を表現することの欠点と固有の失敗に気づくか理解する。彼らは、プログラミング言語が30、29、または28とは対照的に、先月31日間あったことに気づかない(または気にしない)ことや、その逆がTimeSpanを増やすと加算されることを理解しません。

これがこの投稿の中心にある問題です。ほとんどの人はこの違いを気にしないことを理解しています(しかし、私たちの一部がそうであり、私たちの背中にこれを置くことができないことを確認してください)。気にならなかったらよかったのですが、時間とストレスと失望を少しでも軽減できたでしょう。これが煩わしくない場合は、通常はごくわずかな精度で使用する代わりに、相対時間を効率的にテキストで表示するための関数(秒から年まで1〜6ノードにカスタマイズ可能)を使用できます。

残念なことに、タイムスパンを取得して.yearsまたは.monthsを実行すると、何も得られず、.daysだけが得られる実際のタイムスパンオブジェクトがないことに気付きました。これは、timeSpanオブジェクトがtimeSpanが作成された月または年を通知するものを何も持たないためです。したがって、各月の日数が1年、さらにうるう年で変化するため、実際の月数は決してわかりません。

これに対応して、正確な測定値を取得し、ASP.NET Webページで次のようなものを返すことができるように開発した関数を投稿します...

4年、3か月、14日、15時間、18分、24秒前に投稿

あると思いました…

timeSpan.GetActualNumberOf[Months/Days/Hours/etc](もちろん基準日を入力する必要があります)

…このデータ型のメソッドを入力しましたが、ありませんでした。

あなたが本当にしなければならないことは、timeSpanオブジェクトに別のプロパティを作成して、差が計算された基準日を与えることです。そうすれば、上記の素敵な文字列はかなり簡単に計算でき、.year.monthが存在します!

更新:私は以下の私の回答の公式回答とコード使用の詳細を大幅に拡張して更新しました。100%有効な回答とコード(全体)、正確で正確な相対時間/日付、概算なし-ありがとうございます。

25

これがコードでの主な答えです。日付/時刻の精度、秒と分、または秒、分と日、何年にも及ぶ任意の数(6つの部分/セグメントを含む)を取得できることに注意してください。上位2つを指定し、それが1年以上前の場合、「1年3か月前」を返し、2つのセグメントを要求したため、残りは返しません。数時間しか経過していない場合は、「2時間1分前」のみが返されます。もちろん、1、2、3、4、5、または6セグメントを指定した場合も同じ規則が適用されます(秒、分、時間、日、月、年は6タイプのみであるため、最大値は6です)。また、1分以上かどうかに応じて、「分」と「分」のような文法の問題をすべてのタイプで同じに修正し、生成された「文字列」は常に文法的に正しいものになります。

使用例をいくつか示します。bAllowSegmentsは、表示するセグメントの数を識別します...つまり、3の場合、返される文字列は(例として)... "3 years, 2 months and 13 days"(時間、分、秒は含まれません)上位3つの時間カテゴリが返されるため)、ただし、日付が数日前などの新しい日付の場合、同じセグメント(3)を指定すると、代わりに"4 days, 1 hour and 13 minutes ago"が返されるため、すべてが考慮されます。 !

bAllowSegmentsが2の場合は"3 years and 2 months"を返し、6(最大値)の場合は"3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds"を返しますが、NEVER RETURNのように"0 years, 0 months, 0 days, 3 hours, 2 minutes and 13 seconds ago"になることを思い出してください。上位3セグメントには日付データがなく、6セグメントを指定しても無視されますので、心配しないでください:)もちろん、0が含まれるセグメントがある場合、文字列を形成するときにそれが考慮され、"3 days and 4 seconds ago"として表示され、「0時間」の部分は無視されます。楽しんでコメントしてください。

 Public Function RealTimeUntilNow(ByVal dt As DateTime, Optional ByVal bAllowSegments As Byte = 2) As String
  ' bAllowSegments identifies how many segments to show... ie: if 3, then return string would be (as an example)...
  ' "3 years, 2 months and 13 days" the top 3 time categories are returned, if bAllowSegments is 2 it would return
  ' "3 years and 2 months" and if 6 (maximum value) would return "3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds"
  Dim rYears, rMonths, rDays, rHours, rMinutes, rSeconds As Int16
  Dim dtNow = DateTime.Now
  Dim daysInBaseMonth = Date.DaysInMonth(dt.Year, dt.Month)

  rYears = dtNow.Year - dt.Year
  rMonths = dtNow.Month - dt.Month
  If rMonths < 0 Then rMonths += 12 : rYears -= 1 ' add 1 year to months, and remove 1 year from years.
  rDays = dtNow.Day - dt.Day
  If rDays < 0 Then rDays += daysInBaseMonth : rMonths -= 1
  rHours = dtNow.Hour - dt.Hour
  If rHours < 0 Then rHours += 24 : rDays -= 1
  rMinutes = dtNow.Minute - dt.Minute
  If rMinutes < 0 Then rMinutes += 60 : rHours -= 1
  rSeconds = dtNow.Second - dt.Second
  If rSeconds < 0 Then rSeconds += 60 : rMinutes -= 1

  ' this is the display functionality
  Dim sb As StringBuilder = New StringBuilder()
  Dim iSegmentsAdded As Int16 = 0

  If rYears > 0 Then sb.Append(rYears) : sb.Append(" year" & If(rYears <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rMonths > 0 Then sb.AppendFormat(rMonths) : sb.Append(" month" & If(rMonths <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rDays > 0 Then sb.Append(rDays) : sb.Append(" day" & If(rDays <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rHours > 0 Then sb.Append(rHours) : sb.Append(" hour" & If(rHours <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rMinutes > 0 Then sb.Append(rMinutes) : sb.Append(" minute" & If(rMinutes <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rSeconds > 0 Then sb.Append(rSeconds) : sb.Append(" second" & If(rSeconds <> 1, "s", "") & "") : iSegmentsAdded += 1

parseAndReturn:

  ' if the string is entirely empty, that means it was just posted so its less than a second ago, and an empty string getting passed will cause an error
  ' so we construct our own meaningful string which will still fit into the "Posted * ago " syntax...

  If sb.ToString = "" Then sb.Append("less than 1 second")

  Return ReplaceLast(sb.ToString.TrimEnd(" ", ",").ToString, ",", " and")

 End Function

もちろん、「ReplaceLast」関数が必要です。これは、ソース文字列と、置換する必要があるものを指定する引数と、置換するものを指定する別の引数を必要とし、その文字列の最後の出現のみを置換します...実装していない場合、または実装したくない場合に備えています。ここでは、変更を加えることなく「そのまま」機能します。 reverseit関数が不要になったことは知っていますが(.netに存在します)、ReplaceLastとReverseIt関数は.netより前の日付から引き継がれるため、日付がどのように表示されるかを言い訳にしてください(まだ100%機能し、使用しています) emは10年以上、バグがないことを保証できます)... :)。また、VB6を使用している場合は、ReverseIt()関数(拡張メソッドとして提供)を使用する代わりに、StrReverse(.ReverseIt拡張メソッドで拡張された文字列をラップする)を使用できます。したがって、sReplacable.ReverseItを実行する代わりに、StrReverse()は組み込みのVB6関数であるため、StrReverse(sReplacable)を実行します(まったく同じことを行い、指定された文字列を反転し、それ以上何もしません)。汎用的なReverseIt関数の代わりにStrReverse()を使用する場合は、ReverseIt関数/拡張機能を削除してください。従来のms-visualbasic-dllライブラリをインポートしている限り、.NETでStrReverse()関数を使用できます。どちらの方法でも違いはありません、StrReverse()関数が存在することを知る前に、ReverseIt()を記述し、習慣からそれを使用していました(組み込みのジェネリック関数とは対照的に、鉱山を使用する本当の理由はありません) StrReverse)-実際、StrReverse(または文字列反転関数の同様の新しい.NET固有のバージョン)は、より効率的になるように記述されていると思います:)乾杯。

<Extension()> _ 
Public Function ReplaceLast(ByVal sReplacable As String, ByVal sReplaceWhat As String, ByVal sReplaceWith As String) As String 
    ' let empty string arguments run, incase we dont know if we are sending and empty string or not. 
    sReplacable = sReplacable.ReverseIt 
    sReplacable = Replace(sReplacable, sReplaceWhat.ReverseIt, sReplaceWith.ReverseIt, , 1) ' only does first item on reversed version! 
    Return sReplacable.ReverseIt.ToString 
End Function 

<Extension()> _ 
Public Function ReverseIt(ByVal strS As String, Optional ByVal n As Integer = -1) As String 
    Dim strTempX As String = "", intI As Integer 

    If n > strS.Length Or n = -1 Then n = strS.Length 

    For intI = n To 1 Step -1 
        strTempX = strTempX + Mid(strS, intI, 1) 
    Next intI 

    ReverseIt = strTempX + Right(strS, Len(strS) - n) 

End Function 
5

これは、平均値を使用してC#でこれにいくつかの拡張メソッドを追加する方法です。

public static class TimeSpanExtensions
{
    public static int GetYears(this TimeSpan timespan)
    {
        return (int)(timespan.Days/365.2425);
    }
    public static int GetMonths(this TimeSpan timespan)
    {
        return (int)(timespan.Days/30.436875);
    }
}
26
brianary

あなたが探しているものは確かにTimeSpanが表すものではありません。 TimeSpanは、ベースDateTimeまたはCalendarに関係なく、ティックのカウントとして間隔を表します。

ここでは、新しいDateDifference型の方が理にかなっています。コンストラクタまたはファクトリメソッドは、ベースDateTime、ターゲットDateTime、およびオプションでCalendar(さまざまな差分コンポーネント(年、月など)を計算するためにデフォルトでCultureInfo.CurrentCultureに設定されます)

編集:Noda Time のように見えますが、これに必要なツールがあります— Period クラス "[r] eは、人間の時系列用語で表現された期間を表します:時間、日、週、月など"、特にPeriod.Between(then, now, PeriodUnits.AllUnits)は正確であるようですあなたが求めている計算—しかし、それは必然的にTimeSpanよりはるかに複雑なクラスです。 Noda Time wikiの Key Conceptsページ は、「人間が時間を乱雑にする」方法を説明しています。

天文学と相対性のトリッキーな部分を除いて、人類はまだ交渉するのを困難にしました。私たち全員が時間について話すためにUnixエポックのティックを使用する場合、野田タイムのようなライブラリは必要ありません。

しかし、いいえ、私たちは何年、何ヶ月、何日、何週間も話すのが好きです-そして、何らかの理由で、午後12時(紛らわしいことに午後1時の前に来ます)が大体太陽が最高の時間であるようにしたいので...タイムゾーン

それだけでなく、何ヶ月あるかについて全員が同意するわけではありません。文明が異なれば、年を分割する方法も異なります。また、最初の年は数によって異なります。これらはカレンダーシステムです。

11
Jeffrey Hantin

まあ、もっと遅くて、私が思うことは何もありません;)

すべてを与えるC#関数

そして、これは私の変更されたバージョンです:

private string GetElapsedTime(DateTime from_date, DateTime to_date) {
int years;
int months;
int days;
int hours;
int minutes;
int seconds;
int milliseconds;

//------------------
// Handle the years.
//------------------
years = to_date.Year - from_date.Year;

//------------------------
// See if we went too far.
//------------------------
DateTime test_date = from_date.AddMonths(12 * years);

if (test_date > to_date)
{
    years--;
    test_date = from_date.AddMonths(12 * years);
}

//--------------------------------
// Add months until we go too far.
//--------------------------------
months = 0;

while (test_date <= to_date)
{
    months++;
    test_date = from_date.AddMonths(12 * years + months);
}

months--;

//------------------------------------------------------------------
// Subtract to see how many more days, hours, minutes, etc. we need.
//------------------------------------------------------------------
from_date = from_date.AddMonths(12 * years + months);
TimeSpan remainder = to_date - from_date;
days = remainder.Days;
hours = remainder.Hours;
minutes = remainder.Minutes;
seconds = remainder.Seconds;
milliseconds = remainder.Milliseconds;

return (years > 0 ? years.ToString() + " years " : "") +
       (months > 0 ? months.ToString() + " months " : "") +
       (days > 0 ? days.ToString() + " days " : "") +
       (hours > 0 ? hours.ToString() + " hours " : "") +
       (minutes > 0 ? minutes.ToString() + " minutes " : "");}
5
Marc Roussel

現在のTimeSpanは実際のタイムスパンオブジェクトです。つまり、2008年1月1日の午前1時31分から2008年2月3日の午前6時45分までの時間は、2月5日の時間と同じです。 、2008 1:15 pm 2008年3月9日午後6時59分。探しているのは、実際には2つの日時の違いです。

.MakeMagicHappen.gimmeSomethingPretty.surelyMShasThoughtAboutThisDilemaがシステムの特定のニーズを満たすために、人々がプログラマーとしてあなたを雇うのはそのためです。使用しているフレームワークがすべてを実行すると、会社は1つのボタンを押すだけで、そのシステムが完全に形成され、他のプログラマーと一緒に失業率を上げることができます。

2
Jeff Hornby

.Net 4.5とCultureInfoクラスを使用すると、特定の日付に月と年を追加できます。

DateTime datetime = DateTime.UtcNow;
int years = 15;
int months = 7;

DateTime yearsAgo = CultureInfo.InvariantCulture.Calendar.AddYears(datetime, -years);
DateTime monthsInFuture = CultureInfo.InvariantCulture.Calendar.AddMonths(datetime, months);

入力が多いため、拡張メソッドを作成することをお勧めします。

public static DateTime AddYears(this DateTime datetime, int years)
{
    return CultureInfo.InvariantCulture.Calendar.AddYears(datetime, years);
}

public static DateTime AddMonths(this DateTime datetime, int months)
{
    return CultureInfo.InvariantCulture.Calendar.AddMonths(datetime, months);
}

DateTime yearsAgo = datetime.AddYears(-years);
DateTime monthsInFuture = datetime.AddMonths(months);
2
Zarepheth

次の方法はフレームワークの日付計算に基づいており、Facebookのように読み取り可能な経過時間文字列を返すため、次の方法はかなり信頼でき、簡単だと思います。ちょっとしたポルトガル語と複数の扱いについて申し訳ありませんが、私の場合は必要でした。

public static string ElapsedTime(DateTime dtEvent)
{
    TimeSpan TS = DateTime.Now - dtEvent;

    int intYears = TS.Days / 365;
    int intMonths = TS.Days / 30;
    int intDays = TS.Days;
    int intHours = TS.Hours;
    int intMinutes = TS.Minutes;
    int intSeconds = TS.Seconds;

    if (intYears > 0) return String.Format("há {0} {1}", intYears, (intYears == 1) ? "ano" : "anos");
    else if (intMonths > 0) return String.Format("há {0} {1}", intMonths, (intMonths == 1) ? "mês" : "meses");
    else if (intDays > 0) return String.Format("há {0} {1}", intDays, (intDays == 1) ? "dia" : "dias");
    else if (intHours > 0) return String.Format("há ± {0} {1}", intHours, (intHours == 1) ? "hora" : "horas");
    else if (intMinutes > 0) return String.Format("há ± {0} {1}", intMinutes, (intMinutes == 1) ? "minuto" : "minutos");
    else if (intSeconds > 0) return String.Format("há ± {0} {1}", intSeconds, (intSeconds == 1) ? "segundo" : "segundos");
    else
    {
        return String.Format("em {0} às {1}", dtEvent.ToShortDateString(), dtEvent.ToShortTimeString());
    }
}
1
Tuco

私は受け入れられた回答を受け取り、それをVB.NetからC#に変換し、いくつかの変更/改善も行いました。文字列の最後のインスタンスを置き換えるために使用されていた文字列反転を取り除き、文字列の最後のインスタンスをより直接的に見つけて置き換える拡張メソッドを使用しました。

メソッドの呼び出し方法の例:

PeriodBetween(#2/28/2011#, DateTime.UtcNow, 6)

主な方法:

public static string PeriodBetween(DateTime then, DateTime now, byte numberOfPeriodUnits = 2)
{
    // Translated from VB.Net to C# from: https://stackoverflow.com/a/1956265

    // numberOfPeriodUnits identifies how many time period units to show.
    // If numberOfPeriodUnits = 3, function would return:
    //      "3 years, 2 months and 13 days"
    // If numberOfPeriodUnits = 2, function would return:
    //      "3 years and 2 months"
    // If numberOfPeriodUnits = 6, (maximum value), function would return:
    //      "3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds"

    if (numberOfPeriodUnits > 6 || numberOfPeriodUnits < 1)
    {
        throw new ArgumentOutOfRangeException($"Parameter [{nameof(numberOfPeriodUnits)}] is out of bounds. Valid range is 1 to 6.");
    }

    short Years = 0;
    short Months = 0;
    short Days = 0;
    short Hours = 0;
    short Minutes = 0;
    short Seconds = 0;
    short DaysInBaseMonth = (short)(DateTime.DaysInMonth(then.Year, then.Month));

    Years = (short)(now.Year - then.Year);

    Months = (short)(now.Month - then.Month);
    if (Months < 0)
    {
        Months += 12;
        Years--; // add 1 year to months, and remove 1 year from years.
    }

    Days = (short)(now.Day - then.Day);
    if (Days < 0)
    {
        Days += DaysInBaseMonth;
        Months--;
    }

    Hours = (short)(now.Hour - then.Hour);
    if (Hours < 0)
    {
        Hours += 24;
        Days--;
    }

    Minutes = (short)(now.Minute - then.Minute);
    if (Minutes < 0)
    {
        Minutes += 60;
        Hours--;
    }

    Seconds = (short)(now.Second - then.Second);
    if (Seconds < 0)
    {
        Seconds += 60;
        Minutes--;
    }

    // This is the display functionality.
    StringBuilder TimePeriod = new StringBuilder();
    short NumberOfPeriodUnitsAdded = 0;

    if (Years > 0)
    {
        TimePeriod.Append(Years);
        TimePeriod.Append(" year" + (Years != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Months > 0)
    {
        TimePeriod.AppendFormat(Months.ToString());
        TimePeriod.Append(" month" + (Months != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Days > 0)
    {
        TimePeriod.Append(Days);
        TimePeriod.Append(" day" + (Days != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Hours > 0)
    {
        TimePeriod.Append(Hours);
        TimePeriod.Append(" hour" + (Hours != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Minutes > 0)
    {
        TimePeriod.Append(Minutes);
        TimePeriod.Append(" minute" + (Minutes != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Seconds > 0)
    {
        TimePeriod.Append(Seconds);
        TimePeriod.Append(" second" + (Seconds != 1 ? "s" : "") + "");
        NumberOfPeriodUnitsAdded++;
    }

    ParseAndReturn:
    // If the string is empty, that means the datetime is less than a second in the past.
    // An empty string being passed will cause an error, so we construct our own meaningful
    // string which will still fit into the "Posted * ago " syntax.

    if (TimePeriod.ToString() == "")
    {
        TimePeriod.Append("less than 1 second");
    }

    return TimePeriod.ToString().TrimEnd(' ', ',').ToString().ReplaceLast(",", " and");
}

ReplaceLast拡張メソッド:

public static string ReplaceLast(this string source, string search, string replace)
{
    int pos = source.LastIndexOf(search);

    if (pos == -1)
    {
        return source;
    }

    return source.Remove(pos, search.Length).Insert(pos, replace);
}
0
user1367200