Year.java

/*
 * Copyright (C) 2020-2023 Dipl.-Inform. Kai Hofmann. All rights reserved!
 */
package de.powerstat.validation.values;


import java.util.Objects;

import de.powerstat.validation.interfaces.IValueObject;


/**
 * Year.
 *
 * Not DSGVO relevant.
 *
 * TODO Weeks weeksWithin() = (50, 51,) 52, 53 (CalendarSystem, Country dependend ISO vs US)
 * TODO min, max
 */
public final class Year implements Comparable<Year>, IValueObject
 {
  /**
   * Unsupported calendar system constant.
   */
  private static final String UNSUPPORTED_CALENDAR_SYSTEM = "Unsupported calendar system!";

  /* *
   * Cache for singletons.
   */
  // private static final Map<NTuple2<CalendarSystems, Long>, Year> CACHE = new WeakHashMap<>();

  /**
   * Year of Gregorian calendar reform.
   *
   * TODO Country dependend.
   */
  private static final long BEFORE_GREGORIAN_YEAR = 1582;

  /**
   * Calendar system.
   */
  private final CalendarSystems calendarSystem;

  /**
   * Year.
   */
  private final long year;


  /**
   * Constructor.
   *
   * @param calendarSystem Calendar system
   * @param year Year != 0
   * @throws NullPointerException When calendarSystem is null
   * @throws IndexOutOfBoundsException When the year is 0
   */
  private Year(final CalendarSystems calendarSystem, final long year)
   {
    super();
    Objects.requireNonNull(calendarSystem, "calendarSystem"); //$NON-NLS-1$
    if (year == 0)
     {
      throw new IndexOutOfBoundsException("Year 0 does not exist!"); //$NON-NLS-1$
     }
    this.calendarSystem = calendarSystem;
    this.year = year;
   }


  /**
   * Year factory.
   *
   * @param calendarSystem Calendar system
   * @param year Year != 0
   * @return Year object
   */
  public static Year of(final CalendarSystems calendarSystem, final long year)
   {
    /*
    final NTuple2<CalendarSystems, Long> tuple = NTuple2.of(calendarSystem, year);
    synchronized (Year.class)
     {
      Year obj = Year.CACHE.get(tuple);
      if (obj != null)
       {
        return obj;
       }
      obj = new Year(calendarSystem, year);
      Year.CACHE.put(tuple, obj);
      return obj;
     }
    */
    return new Year(calendarSystem, year);
   }


  /**
   * Gregorian calendar year factory.
   *
   * @param year Year != 0
   * @return Year object
   */
  public static Year of(final long year)
   {
    return of(CalendarSystems.GREGORIAN, year);
   }


  /**
   * Gregorian calendar year factory.
   *
   * @param value Year != 0 string
   * @return Year object
   */
  public static Year of(final String value)
   {
    return of(CalendarSystems.GREGORIAN, Long.parseLong(value));
   }


  /**
   * Returns the value of this Year as an long.
   *
   * @return The numeric value represented by this object after conversion to type long.
   */
  public long longValue()
   {
    return this.year;
   }


  /**
   * Returns the value of this Year as an String.
   *
   * @return The numeric value represented by this object after conversion to type String.
   */
  @Override
  public String stringValue()
   {
    return String.valueOf(this.year);
   }


  /**
   * Months within year.
   *
   * @return Months (12) within year
   */
  public static Months monthsWithin()
   {
    return Months.of(12);
   }


  /**
   * Is julian calendar leap year.
   *
   * @param year Julian calendar year
   * @return true: is leap year; false otherwise
   */
  private static boolean isJulianLeapYear(final long year)
   {
    if (year <= 0)
     {
      return ((-year) % 4) == 1;
     }
    else
     {
      return (year % 4) == 0;
     }
   }


  /**
   * Is gregorian calendar leap year.
   *
   * @param year Gregorian calendar year
   * @return  true: is leap year; false otherwise
   */
  private static boolean isGregorianLeapYear(final long year)
   {
    return (((year % 4) == 0) && (((year % 100) > 0) || ((year % 400) == 0)));
   }


  /**
   * Calendar system dependent leap year.
   *
   * @return true: is leap year; false otherwise
   * @throws IllegalStateException When an unsupported calendar system is used
   */
  public boolean isLeapYear()
   {
    switch (this.calendarSystem)
     {
      case JULIAN:
        return isJulianLeapYear(this.year);
      case GREGORIAN:
        if (this.year < BEFORE_GREGORIAN_YEAR) // Country dependend
         {
          return isJulianLeapYear(this.year);
         }
        else
         {
          return isGregorianLeapYear(this.year);
         }
      default:
        throw new IllegalStateException(UNSUPPORTED_CALENDAR_SYSTEM);
     }
   }


  /**
   * Leap year dependent days within year.
   *
   * @return Days within year
   * @throws IllegalStateException When an unsupported calendar system is used
   */
  public Days daysWithin()
   {
    switch (this.calendarSystem)
     {
      case JULIAN:
        return Days.of(365L + (this.isLeapYear() ? 1 : 0));
      case GREGORIAN:
        if (this.year == BEFORE_GREGORIAN_YEAR) // Country dependend
         {
          return Days.of(365L - 10); // Country dependend
         }
        return Days.of(365L + (this.isLeapYear() ? 1 : 0));
      default:
        throw new IllegalStateException(UNSUPPORTED_CALENDAR_SYSTEM);
    }
   }


  /**
   * Calculate hash code.
   *
   * @return Hash
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode()
   {
    return Objects.hash(this.calendarSystem, this.year);
   }


  /**
   * Is equal with another object.
   *
   * @param obj Object
   * @return true when equal, false otherwise
   * @see java.lang.Object#equals(java.lang.Object)
   */
  @Override
  public boolean equals(final Object obj)
   {
    if (this == obj)
     {
      return true;
     }
    if (!(obj instanceof Year))
     {
      return false;
     }
    final Year other = (Year)obj;
    return this.year == other.year;
   }


  /**
   * Returns the string representation of this Year.
   *
   * The exact details of this representation are unspecified and subject to change, but the following may be regarded as typical:
   *
   * "Year[calendarSystem=GREGORIAN, year=2020]"
   *
   * @return String representation of this Year
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString()
   {
    final var builder = new StringBuilder(28);
    builder.append("Year[calendarSystem=").append(this.calendarSystem).append(", year=").append(this.year).append(']'); //$NON-NLS-1$
    return builder.toString();
   }


  /**
   * Compare with another object.
   *
   * @param obj Object to compare with
   * @return 0: equal; 1: greater; -1: smaller
   * @throws IllegalStateException When the calendarSystems of the two years are not equal
   * @see java.lang.Comparable#compareTo(java.lang.Object)
   */
  @Override
  public int compareTo(final Year obj)
   {
    Objects.requireNonNull(obj, "obj"); //$NON-NLS-1$
    if (this.calendarSystem.compareTo(obj.calendarSystem) != 0)
     {
      throw new IllegalStateException("CalendarSystems are not equal!");
     }
    return Long.compare(this.year, obj.year);
   }


  /**
   * Add years to this year.
   *
   * @param years Years to add to this year
   * @return New year after adding the years to this year with same calendarSystem
   * @throws ArithmeticException In case of an overflow
   */
  public Year add(final Years years)
   {
    long newYear = Math.addExact(this.year, years.longValue());
    if ((this.year < 0) && (newYear >= 0))
     {
      newYear = Math.incrementExact(newYear); // Because there is no year 0!
     }
    return Year.of(this.calendarSystem, newYear);
   }


  /**
   * Subtract years from this year.
   *
   * @param years Years to subtract from this year
   * @return New year after subtracting years from this year with same calendarSystem
   * @throws ArithmeticException In case of an underflow
   */
  public Year subtract(final Years years)
   {
    long newYear = Math.subtractExact(this.year, years.longValue());
    if ((this.year > 0) && (newYear <= 0))
     {
      newYear = Math.decrementExact(newYear); // Because there is no year 0!
     }
    return Year.of(this.calendarSystem, newYear);
   }


  /**
   * Increment this year.
   *
   * @return New year after incrementing this year
   * @throws ArithmeticException In case of an overflow
   */
  public Year increment()
   {
    long newYear = Math.incrementExact(this.year);
    if (this.year == -1)
     {
      newYear = Math.incrementExact(newYear); // Because there is no year 0!
     }
    return Year.of(newYear);
   }


  /**
   * Decrement this year.
   *
   * @return New year after decrement this year
   * @throws ArithmeticException In case of an overflow
   */
  public Year decrement()
   {
    long newYear = Math.decrementExact(this.year);
    if (this.year == 1)
     {
      newYear = Math.decrementExact(newYear); // Because there is no year 0!
     }
    return Year.of(newYear);
   }

 }