using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Newtonsoft.Json;
using StardewModdingAPI.Framework;
using StardewValley;
namespace StardewModdingAPI.Utilities
{
/// Represents a Stardew Valley date.
public class SDate : IEquatable
{
/*********
** Fields
*********/
/// 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;
/// The core SMAPI translations.
internal static Translator? Translations;
/*********
** Accessors
*********/
/// The day of month.
public int Day { get; }
/// The season name.
public string Season { get; }
/// The index of the season (where 0 is spring, 1 is summer, 2 is fall, and 3 is winter).
/// This is used in some game calculations (e.g. seasonal game sprites) and methods (e.g. ).
[JsonIgnore]
public int SeasonIndex { get; }
/// The year.
public int Year { get; }
/// The day of week.
[JsonIgnore]
public DayOfWeek DayOfWeek { get; }
/// The number of days since the game began (starting at 1 for the first day of spring in Y1).
[JsonIgnore]
public int DaysSinceStart { 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).
[JsonConstructor]
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 date from the number of days after 0 spring Y1.
/// The number of days since 0 spring Y1.
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.");
}
}
/// Get a date from a game date instance.
/// The world date.
[return: NotNullIfNotNull("date")]
public static SDate? From(WorldDate? date)
{
if (date == null)
return null;
return new SDate(date.DayOfMonth, date.Season, date.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.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);
}
/// Get a game date representation of the date.
public WorldDate ToWorldDate()
{
return new WorldDate(this.Year, this.Season, this.Day);
}
/// Get an untranslated 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}";
}
/// Get a translated string representation of the date in the current game locale.
/// Whether to get a string which includes the year number.
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
****/
/// 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 this.DaysSinceStart;
}
/****
** 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?.DaysSinceStart == other?.DaysSinceStart;
}
/// 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?.DaysSinceStart != other?.DaysSinceStart;
}
/// 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?.DaysSinceStart > other?.DaysSinceStart;
}
/// 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?.DaysSinceStart >= other?.DaysSinceStart;
}
/// 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?.DaysSinceStart <= other?.DaysSinceStart;
}
/// 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?.DaysSinceStart < other?.DaysSinceStart;
}
/*********
** 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).
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "The nullability is validated in this constructor.")]
private SDate(int day, string season, int year, bool allowDayZero)
{
season = season?.Trim().ToLowerInvariant()!; // null-checked below
// 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);
}
/// Get whether a date represents 0 spring Y1, which is the date during the in-game intro.
/// The day of month.
/// The normalized 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 a given date.
/// The day of month.
private DayOfWeek GetDayOfWeek(int day)
{
return (day % 7) switch
{
0 => DayOfWeek.Sunday,
1 => DayOfWeek.Monday,
2 => DayOfWeek.Tuesday,
3 => DayOfWeek.Wednesday,
4 => DayOfWeek.Thursday,
5 => DayOfWeek.Friday,
6 => DayOfWeek.Saturday,
_ => 0
};
}
/// Get the number of days since the game began (starting at 1 for the first day of spring in Y1).
/// The day of month.
/// The season name.
/// The year.
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;
}
/// Get a season index.
/// The season name.
/// The current season wasn't recognized.
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;
}
}
}