using System; using System.Linq; using StardewValley; namespace StardewModdingAPI.Utilities { /// Represents a Stardew Valley date. public class SDate : IEquatable { /********* ** Properties *********/ /// The internal season names in order. private readonly string[] Seasons = { "spring", "summer", "fall", "winter" }; /// The number of seasons in a year. private int SeasonsInYear => this.Seasons.Length; /// The number of days in a season. private readonly int DaysInSeason = 28; /********* ** Accessors *********/ /// The day of month. public int Day { get; } /// The season name. public string Season { get; } /// The year. public int Year { get; } /// The day of week. public DayOfWeek DayOfWeek { get; } /********* ** Public methods *********/ /// Construct an instance. /// The day of month. /// The season name. /// One of the arguments has an invalid value (like day 35). public SDate(int day, string season) : this(day, season, Game1.year) { } /// Construct an instance. /// The day of month. /// The season name. /// The year. /// One of the arguments has an invalid value (like day 35). public SDate(int day, string season, int year) : this(day, season, year, allowDayZero: false) { } /// Get the current in-game date. public static SDate Now() { return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year, allowDayZero: true); } /// Get a new date with the given number of days added. /// The number of days to add. /// Returns the resulting date. /// The offset would result in an invalid date (like year 0). public SDate AddDays(int offset) { // get new hash code int hashCode = this.GetHashCode() + offset; if (hashCode < 1) throw new ArithmeticException($"Adding {offset} days to {this} would result in a date before 01 spring Y1."); // get day int day = hashCode % 28; if (day == 0) day = 28; // get season index int seasonIndex = hashCode / 28; if (seasonIndex > 0 && hashCode % 28 == 0) seasonIndex -= 1; seasonIndex %= 4; // get year int year = hashCode / (this.Seasons.Length * this.DaysInSeason) + 1; // create date return new SDate(day, this.Seasons[seasonIndex], year); } /// Get a string representation of the date. This is mainly intended for debugging or console messages. public override string ToString() { return $"{this.Day:00} {this.Season} Y{this.Year}"; } /**** ** IEquatable ****/ /// Get whether this instance is equal to another. /// The other value to compare. public bool Equals(SDate other) { return this == other; } /// Get whether this instance is equal to another. /// The other value to compare. public override bool Equals(object obj) { return obj is SDate other && this == other; } /// Get a hash code which uniquely identifies a date. public override int GetHashCode() { // return the number of days since 01 spring Y1 (inclusively) int yearIndex = this.Year - 1; return yearIndex * this.DaysInSeason * this.SeasonsInYear + this.GetSeasonIndex() * this.DaysInSeason + this.Day; } /**** ** Operators ****/ /// Get whether one date is equal to another. /// The base date to compare. /// The other date to compare. /// The equality of the dates public static bool operator ==(SDate date, SDate other) { return date?.GetHashCode() == other?.GetHashCode(); } /// Get whether one date is not equal to another. /// The base date to compare. /// The other date to compare. public static bool operator !=(SDate date, SDate other) { return date?.GetHashCode() != other?.GetHashCode(); } /// Get whether one date is more than another. /// The base date to compare. /// The other date to compare. public static bool operator >(SDate date, SDate other) { return date?.GetHashCode() > other?.GetHashCode(); } /// Get whether one date is more than or equal to another. /// The base date to compare. /// The other date to compare. public static bool operator >=(SDate date, SDate other) { return date?.GetHashCode() >= other?.GetHashCode(); } /// Get whether one date is less than or equal to another. /// The base date to compare. /// The other date to compare. public static bool operator <=(SDate date, SDate other) { return date?.GetHashCode() <= other?.GetHashCode(); } /// Get whether one date is less than another. /// The base date to compare. /// The other date to compare. public static bool operator <(SDate date, SDate other) { return date?.GetHashCode() < other?.GetHashCode(); } /********* ** Private methods *********/ /// Construct an instance. /// The day of month. /// The season name. /// The year. /// Whether to allow 0 spring Y1 as a valid date. /// One of the arguments has an invalid value (like day 35). private SDate(int day, string season, int year, bool allowDayZero) { // validate if (season == null) throw new ArgumentNullException(nameof(season)); if (!this.Seasons.Contains(season)) throw new ArgumentException($"Unknown season '{season}', must be one of [{string.Join(", ", this.Seasons)}]."); if (day < 0 || day > this.DaysInSeason) throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); if(day == 0 && !(allowDayZero && this.IsDayZero(day, season, year))) throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); if (year < 1) throw new ArgumentException($"Invalid year '{year}', must be at least 1."); // initialise this.Day = day; this.Season = season; this.Year = year; this.DayOfWeek = this.GetDayOfWeek(); } /// Get whether a date represents 0 spring Y1, which is the date during the in-game intro. /// The day of month. /// The season name. /// The year. private bool IsDayZero(int day, string season, int year) { return day == 0 && season == "spring" && year == 1; } /// Get the day of week for the current date. private DayOfWeek GetDayOfWeek() { switch (this.Day % 7) { case 0: return DayOfWeek.Sunday; case 1: return DayOfWeek.Monday; case 2: return DayOfWeek.Tuesday; case 3: return DayOfWeek.Wednesday; case 4: return DayOfWeek.Thursday; case 5: return DayOfWeek.Friday; case 6: return DayOfWeek.Saturday; default: return 0; } } /// Get the current season index. /// The current season wasn't recognised. private int GetSeasonIndex() { int index = Array.IndexOf(this.Seasons, this.Season); if (index == -1) throw new InvalidOperationException($"The current season '{this.Season}' wasn't recognised."); return index; } } }