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;
}
}
}