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; }
#if !SMAPI_1_x
/// The day of week.
public DayOfWeek DayOfWeek { get; }
#endif
/*********
** 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)
{
// 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 < 1 || day > this.DaysInSeason)
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 the current in-game date.
public static SDate Now()
{
return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year);
}
/// 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)
{
// simple case
int day = this.Day + offset;
string season = this.Season;
int year = this.Year;
// handle season transition
if (day > this.DaysInSeason || day < 1)
{
// get season index
int curSeasonIndex = this.GetSeasonIndex();
// get season offset
int seasonOffset = day / this.DaysInSeason;
if (day < 1)
seasonOffset -= 1;
// get new date
day = this.GetWrappedIndex(day, this.DaysInSeason);
season = this.Seasons[this.GetWrappedIndex(curSeasonIndex + seasonOffset, this.Seasons.Length)];
year += seasonOffset / this.Seasons.Length;
}
// validate
if (year < 1)
throw new ArithmeticException($"Adding {offset} days to {this} would result in invalid date {day:00} {season} {year}.");
// return new date
return new SDate(day, season, 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
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
*********/
/// 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;
}
/// Get the real index in an array which should be treated as a two-way loop.
/// The index in the looped array.
/// The number of elements in the array.
private int GetWrappedIndex(int index, int length)
{
int wrapped = index % length;
if (wrapped < 0)
wrapped += length;
return wrapped;
}
}
}