using System; using System.Collections.Generic; using System.Text; namespace CSharpVitamins { /// /// /// public struct Age : IFormattable, IComparable, IComparable, IComparable { public static readonly Age Empty = new Age(DateTime.MinValue, DateTime.MinValue); #region Private Variables DateTime _advent; DateTime _terminus; int _years; int _months; int _weeks; int _days; #endregion #region Constructors /// /// Creates a new instance of Age starting from the given date until now /// /// public Age(DateTime advent) : this(advent, getNowFromKind(advent)) { } /// /// Creates a new instance of Age starting from the given date until now /// /// /// public Age(DateTime advent, DateTime terminus) { _advent = advent; _terminus = terminus; _years = 0; _months = 0; _weeks = 0; _days = 0; updateInternal(); } #endregion #region Properties /// /// Gets the number of years old /// public int Years { get { return _years; } } /// /// Gets the number of months old /// public int Months { get { return _months; } } /// /// Gets the number of weeks old /// public int Weeks { get { return _weeks; } } /// /// Gets the number of days old /// public int Days { get { return _days; } } /// /// Gets/Sets the start of the age /// public DateTime Advent { get { return _advent; } set { _advent = value; updateInternal(); } } /// /// Gets/Sets the end of the age /// public DateTime Terminus { get { return _terminus; } set { _terminus = value; updateInternal(); } } /// /// Gets the elapsed time /// public TimeSpan Elapsed { get { return new TimeSpan(_terminus.Ticks - _advent.Ticks); } } /// /// Gets a value indicating that the age is empty (both advent and terminus dates are MinValue) /// /// public bool IsEmpty { get { return _advent == DateTime.MinValue && _terminus == DateTime.MinValue; } } #endregion #region Update /// /// Updates the age to now /// public Age Update() { return Update(getNowFromKind(_advent)); } /// /// Updates the age to the given terminus /// /// public Age Update(DateTime terminus) { return new Age(_advent, terminus); } /// /// Updates the values for years, months, weeks and days /// void updateInternal() { Extract(_advent, _terminus, out _years, out _months, out _weeks, out _days); } #endregion #region Extract /// /// Calculates the years, months, weeks and days within the two given dates /// /// /// Implementation based on http://tommycarlier.blogspot.com/2006/02/years-months-and-days-between-2-dates.html#links /// /// /// /// /// /// /// public static void Extract(DateTime advent, DateTime terminus, out int years, out int months, out int weeks, out int days) { /// only extracts down to 'days', so the TimeOfDay needs to be /// normalised for this calculation TimeSpan time = advent.TimeOfDay; if (time > TimeSpan.Zero) { advent = advent.Subtract(time); terminus = terminus.Subtract(time); } years = terminus.Year - advent.Year; months = terminus.Month - advent.Month; days = terminus.Day - advent.Day; weeks = 0; if (days < 0) months -= 1; while (months < 0) { months += 12; years -= 1; } TimeSpan span = terminus - advent.AddYears(years).AddMonths(months); days = (int)Math.Floor(span.TotalDays); if (days > 0) { weeks = days / 7; days = days % 7; } } #endregion #region ToString /// /// /// /// public override string ToString() { return ToString(0, true); } /// /// /// /// /// public string ToString(int significantPlaces) { return ToString(significantPlaces, true); } /// /// /// /// /// /// public string ToString(int significantPlaces, bool includeTime) { if (IsEmpty) { return string.Empty; } int max = significantPlaces < 1 ? 10 : significantPlaces; int parts = 0; StringBuilder result = new StringBuilder(); if (_years > 0 && parts < max) { result.AppendFormat(" {0} year{1}", _years, plural(_years)); ++parts; } if (_months > 0 && parts < max) { result.AppendFormat(" {0} month{1}", _months, plural(_months)); ++parts; } if (_weeks > 0 && parts < max) { result.AppendFormat(" {0} week{1}", _weeks, plural(_weeks)); ++parts; } if (_days > 0 && parts < max) { result.AppendFormat(" {0} day{1}", _days, plural(_days)); ++parts; } if (includeTime) { TimeSpan time = Elapsed; if (time.Hours != 0 && parts < max) { result.AppendFormat(" {0} hour{1}", time.Hours, plural(time.Hours)); ++parts; } if (time.Minutes != 0 && parts < max) { result.AppendFormat(" {0} minute{1}", time.Minutes, plural(time.Minutes)); ++parts; } if (time.Seconds != 0 && parts < max) { result.AppendFormat(" {0} second{1}", time.Seconds, plural(time.Seconds)); ++parts; } } return result.Length == 0 ? includeTime ? "less than a second" : "less than a day" : result.ToString().Trim(); } /// /// /// /// /// public string ToString(string format) { return ToString(format, System.Globalization.CultureInfo.CurrentCulture); } #region IFormattable Members /// /// /// /// /// /// public string ToString(string format, IFormatProvider provider) { /// TODO: Finish implmenting the provider if (string.IsNullOrEmpty(format)) format = "g"; char first = format[0]; if (char.ToLower(first) == 'g') { int parts = 0; if (format.Length > 1 && char.IsDigit(format[1])) parts = int.Parse(format[1].ToString()); return ToString(parts); } else if (char.IsDigit(first)) { int parts = int.Parse(first.ToString()); return ToString(parts); } throw new FormatException("Could not parse the Age format: " + format); } #endregion #endregion #region getNowFromKind /// /// Gets the current date/time for universal vrs local time /// /// /// static DateTime getNowFromKind(DateTime time) { return time.Kind == DateTimeKind.Utc ? DateTime.UtcNow : DateTime.Now; } #endregion #region plural /// /// Returns a "s" for numbers other than 0 otherwise an empty string. /// /// /// string plural(int number) { return number == 1 ? string.Empty : "s"; } #endregion #region Format /// /// /// /// /// public static string Format(DateTime advent) { return Format(advent, getNowFromKind(advent), 0); } /// /// /// /// /// /// public static string Format(DateTime advent, DateTime terminus) { return Format(advent, terminus, 0); } /// /// /// /// /// /// public static string Format(DateTime advent, int parts) { return Format(advent, getNowFromKind(advent), parts); } /// /// /// /// /// /// /// public static string Format(DateTime advent, DateTime terminus, int parts) { return new Age(advent, terminus).ToString(parts); } #endregion #region IComparable Members /// /// /// /// /// public int CompareTo(object obj) { if (obj is Age) { return CompareTo((Age)obj); } else if (obj is TimeSpan) { return CompareTo((TimeSpan)obj); } throw new ArgumentException(string.Format("The object '{0}' is of the wrong type for comparison.", obj.GetType(), "obj")); } /// /// /// /// /// public int CompareTo(Age other) { return CompareTo(other.Elapsed); } /// /// /// /// /// public int CompareTo(TimeSpan other) { return Elapsed.CompareTo(other); } #endregion #region GetHashCode (Commented Out) ///// ///// ///// ///// //public override int GetHashCode() //{ // return _advent.GetHashCode() ^ _terminus.GetHashCode(); //} #endregion #region Equals (Commented Out) ///// ///// Tests weather the object is of an equal age by comparing their elapsed timespans. ///// ///// ///// //public override bool Equals( object obj ) //{ // if( obj is Age ) // { // return this == (Age)obj; // } // else if( obj is TimeSpan ) // { // return this.Elapsed == (TimeSpan)obj; // } // return false; //} #endregion #region Operators #region "==" and "!=" (Commented Out) ///// ///// ///// ///// ///// There is some controversy on overriding the Equals operator. ///// ///// ///// ///// //public static bool operator ==( Age x, Age y ) //{ // if( (object)x == null ) return (object)y == null; // return x.Elapsed == y.Elapsed; //} ///// ///// ///// ///// ///// ///// //public static bool operator !=( Age x, Age y ) //{ // return !( x == y ); //} #endregion /// /// /// /// /// /// public static bool operator >(Age x, Age y) { if ((object)y == null) return (object)x != null; return x.Elapsed > y.Elapsed; } /// /// /// /// /// /// public static bool operator <(Age x, Age y) { if ((object)x == null) return (object)y != null; return x.Elapsed < y.Elapsed; } /// /// /// /// /// /// public static bool operator >=(Age x, Age y) { if ((object)y == null) return (object)x != null; return x.Elapsed >= y.Elapsed; } /// /// /// /// /// /// public static bool operator <=(Age x, Age y) { if ((object)x == null) return (object)y != null; return x.Elapsed <= y.Elapsed; } /// /// /// /// /// public static implicit operator TimeSpan(Age age) { return age.Elapsed; } #endregion } }