using System; using System.Linq; using Newtonsoft.Json; using StardewModdingAPI.Framework; using StardewValley; namespace StardewModdingAPI.Utilities { /// <summary>Represents a Stardew Valley date.</summary> public class SDate : IEquatable<SDate> { /********* ** Fields *********/ /// <summary>The internal season names in order.</summary> private readonly string[] Seasons = { "spring", "summer", "fall", "winter" }; /// <summary>The number of seasons in a year.</summary> private int SeasonsInYear => this.Seasons.Length; /// <summary>The number of days in a season.</summary> private readonly int DaysInSeason = 28; /// <summary>The core SMAPI translations.</summary> internal static Translator Translations; /********* ** Accessors *********/ /// <summary>The day of month.</summary> public int Day { get; } /// <summary>The season name.</summary> public string Season { get; } /// <summary>The index of the season (where 0 is spring, 1 is summer, 2 is fall, and 3 is winter).</summary> /// <remarks>This is used in some game calculations (e.g. seasonal game sprites) and methods (e.g. <see cref="Utility.getSeasonNameFromNumber"/>).</remarks> [JsonIgnore] public int SeasonIndex { get; } /// <summary>The year.</summary> public int Year { get; } /// <summary>The day of week.</summary> [JsonIgnore] public DayOfWeek DayOfWeek { get; } /// <summary>The number of days since the game began (starting at 1 for the first day of spring in Y1).</summary> [JsonIgnore] public int DaysSinceStart { get; } /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="day">The day of month.</param> /// <param name="season">The season name.</param> /// <exception cref="ArgumentException">One of the arguments has an invalid value (like day 35).</exception> public SDate(int day, string season) : this(day, season, Game1.year) { } /// <summary>Construct an instance.</summary> /// <param name="day">The day of month.</param> /// <param name="season">The season name.</param> /// <param name="year">The year.</param> /// <exception cref="ArgumentException">One of the arguments has an invalid value (like day 35).</exception> [JsonConstructor] public SDate(int day, string season, int year) : this(day, season, year, allowDayZero: false) { } /// <summary>Get the current in-game date.</summary> public static SDate Now() { return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year, allowDayZero: true); } /// <summary>Get a date from the number of days after 0 spring Y1.</summary> /// <param name="daysSinceStart">The number of days since 0 spring Y1.</param> public static SDate FromDaysSinceStart(int daysSinceStart) { try { return new SDate(0, "spring", 1, allowDayZero: true).AddDays(daysSinceStart); } catch (ArithmeticException) { throw new ArgumentException($"Invalid daysSinceStart '{daysSinceStart}', must be at least 1."); } } /// <summary>Get a date from a game date instance.</summary> /// <param name="date">The world date.</param> public static SDate From(WorldDate date) { if (date == null) return null; return new SDate(date.DayOfMonth, date.Season, date.Year, allowDayZero: true); } /// <summary>Get a new date with the given number of days added.</summary> /// <param name="offset">The number of days to add.</param> /// <returns>Returns the resulting date.</returns> /// <exception cref="ArithmeticException">The offset would result in an invalid date (like year 0).</exception> public SDate AddDays(int offset) { // get new hash code int hashCode = this.DaysSinceStart + 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 = (int)Math.Ceiling(hashCode / (this.Seasons.Length * this.DaysInSeason * 1m)); // create date return new SDate(day, this.Seasons[seasonIndex], year); } /// <summary>Get a game date representation of the date.</summary> public WorldDate ToWorldDate() { return new WorldDate(this.Year, this.Season, this.Day); } /// <summary>Get an untranslated string representation of the date. This is mainly intended for debugging or console messages.</summary> public override string ToString() { return $"{this.Day:00} {this.Season} Y{this.Year}"; } /// <summary>Get a translated string representation of the date in the current game locale.</summary> /// <param name="withYear">Whether to get a string which includes the year number.</param> public string ToLocaleString(bool withYear = true) { // get fallback translation from game string fallback = Utility.getDateStringFor(this.Day, this.SeasonIndex, this.Year); if (SDate.Translations == null) return fallback; // get short format string seasonName = Utility.getSeasonNameFromNumber(this.SeasonIndex); return SDate.Translations .Get(withYear ? "generic.date-with-year" : "generic.date", new { day = this.Day, year = this.Year, season = seasonName, seasonLowercase = seasonName?.ToLower() }) .Default(fallback); } /**** ** IEquatable ****/ /// <summary>Get whether this instance is equal to another.</summary> /// <param name="other">The other value to compare.</param> public bool Equals(SDate other) { return this == other; } /// <summary>Get whether this instance is equal to another.</summary> /// <param name="obj">The other value to compare.</param> public override bool Equals(object obj) { return obj is SDate other && this == other; } /// <summary>Get a hash code which uniquely identifies a date.</summary> public override int GetHashCode() { return this.DaysSinceStart; } /**** ** Operators ****/ /// <summary>Get whether one date is equal to another.</summary> /// <param name="date">The base date to compare.</param> /// <param name="other">The other date to compare.</param> /// <returns>The equality of the dates</returns> public static bool operator ==(SDate date, SDate other) { return date?.DaysSinceStart == other?.DaysSinceStart; } /// <summary>Get whether one date is not equal to another.</summary> /// <param name="date">The base date to compare.</param> /// <param name="other">The other date to compare.</param> public static bool operator !=(SDate date, SDate other) { return date?.DaysSinceStart != other?.DaysSinceStart; } /// <summary>Get whether one date is more than another.</summary> /// <param name="date">The base date to compare.</param> /// <param name="other">The other date to compare.</param> public static bool operator >(SDate date, SDate other) { return date?.DaysSinceStart > other?.DaysSinceStart; } /// <summary>Get whether one date is more than or equal to another.</summary> /// <param name="date">The base date to compare.</param> /// <param name="other">The other date to compare.</param> public static bool operator >=(SDate date, SDate other) { return date?.DaysSinceStart >= other?.DaysSinceStart; } /// <summary>Get whether one date is less than or equal to another.</summary> /// <param name="date">The base date to compare.</param> /// <param name="other">The other date to compare.</param> public static bool operator <=(SDate date, SDate other) { return date?.DaysSinceStart <= other?.DaysSinceStart; } /// <summary>Get whether one date is less than another.</summary> /// <param name="date">The base date to compare.</param> /// <param name="other">The other date to compare.</param> public static bool operator <(SDate date, SDate other) { return date?.DaysSinceStart < other?.DaysSinceStart; } /********* ** Private methods *********/ /// <summary>Construct an instance.</summary> /// <param name="day">The day of month.</param> /// <param name="season">The season name.</param> /// <param name="year">The year.</param> /// <param name="allowDayZero">Whether to allow 0 spring Y1 as a valid date.</param> /// <exception cref="ArgumentException">One of the arguments has an invalid value (like day 35).</exception> 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."); // initialize this.Day = day; this.Season = season; this.SeasonIndex = this.GetSeasonIndex(season); this.Year = year; this.DayOfWeek = this.GetDayOfWeek(day); this.DaysSinceStart = this.GetDaysSinceStart(day, season, year); } /// <summary>Get whether a date represents 0 spring Y1, which is the date during the in-game intro.</summary> /// <param name="day">The day of month.</param> /// <param name="season">The season name.</param> /// <param name="year">The year.</param> private bool IsDayZero(int day, string season, int year) { return day == 0 && season == "spring" && year == 1; } /// <summary>Get the day of week for a given date.</summary> /// <param name="day">The day of month.</param> private DayOfWeek GetDayOfWeek(int day) { switch (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; } } /// <summary>Get the number of days since the game began (starting at 1 for the first day of spring in Y1).</summary> /// <param name="day">The day of month.</param> /// <param name="season">The season name.</param> /// <param name="year">The year.</param> private int GetDaysSinceStart(int day, string season, int year) { // return the number of days since 01 spring Y1 (inclusively) int yearIndex = year - 1; return yearIndex * this.DaysInSeason * this.SeasonsInYear + this.GetSeasonIndex(season) * this.DaysInSeason + day; } /// <summary>Get a season index.</summary> /// <param name="season">The season name.</param> /// <exception cref="InvalidOperationException">The current season wasn't recognized.</exception> private int GetSeasonIndex(string season) { int index = Array.IndexOf(this.Seasons, season); if (index == -1) throw new InvalidOperationException($"The season '{season}' wasn't recognized."); return index; } } }