Quarter.java

package edu.ucsb.cs156.courses.models;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Represents a UCSB quarter. Allows easy conversion between QYY alphanumeric format (F19, W20, S20,
 * M20) and YYYYQ numerical format (20194, 20201, 20202, 20203) as well as incrementing and
 * decrementing.
 */
public class Quarter {

  private int yyyyq; // YYYYQ where Q = 1, 2, 3 or 4

  public Quarter(int yyyyq) {
    setValue(yyyyq);
  }

  public int getValue() {
    return this.yyyyq;
  }

  public void setValue(int yyyyq) {
    if (invalidQtr(yyyyq))
      throw new IllegalArgumentException(
          "Quarter constructor requires a integer ending in 1,2,3 or 4");
    this.yyyyq = yyyyq;
  }

  /**
   * Construct a Quarter object from a string s, either in QYY or YYYYQ format. If s is of length
   * three, QYY format is expected, if 5 then YYYYQ format is expected. Otherwise an
   * IllegalArgumentException is thrown.
   *
   * @param s Quarter either in QYY or YYYYQ format
   */
  public Quarter(String s) {

    switch (s.length()) {
      case 3:
        this.yyyyq = qyyToyyyyQ(s);
        return;
      case 5:
        this.yyyyq = yyyyqToInt(s);
        return;
      default:
        throw new IllegalArgumentException("Quarter shoudl be in QYY or YYYYQ format");
    }
  }

  public String getYY() {
    return getYY(this.yyyyq);
  }

  public String getYYYY() {
    return getYYYY(this.yyyyq);
  }

  public String getYYYYQ() {
    return String.format("%d", this.yyyyq);
  }

  public String toString() {
    return yyyyqToQyy(this.yyyyq);
  }

  public String getQ() {
    return getQ(this.yyyyq);
  }

  private static boolean invalidQtr(int value) {
    int index = value % 10;
    return (index < 1) || (index > 4);
  }

  /**
   * Advance to the next quarter, and return the value of that quarter as an int.
   *
   * @return the new getValue() for the quarter, i.e. quarter as in in yyyyq format
   */
  public int increment() {
    int q = this.yyyyq % 10;
    int yyyy = this.yyyyq / 10;

    setValue((q == 4) ? (((yyyy + 1) * 10) + 1) : (this.yyyyq + 1));
    return this.yyyyq;
  }

  /**
   * Subtract one from current quarter, and return the value of that quarter as an int.
   *
   * @return the new getValue() for the quarter, i.e. quarter as in in yyyyq format
   */
  public int decrement() {
    int q = this.yyyyq % 10;
    int yyyy = this.yyyyq / 10;

    setValue((q == 1) ? (((yyyy - 1) * 10) + 4) : (this.yyyyq - 1));
    return this.yyyyq;
  }

  /**
   * Convert yyyyq as string to int, throwing exception if format is incorrect
   *
   * @param yyyyq String in yyyyq format (e.g. 20194 for F19 (Fall 2019))
   * @throws IllegalArgumentException
   * @return int representation of quarter
   */
  public static int yyyyqToInt(String yyyyq) {
    String errorMsg =
        "Argument should be a string representation of a five digit integer yyyyq ending in 1,2,3 or 4";
    int result = 0;
    try {
      result = Integer.parseInt(yyyyq);
    } catch (Exception e) {
      throw new IllegalArgumentException(errorMsg);
    }
    if (invalidQtr(result)) {
      throw new IllegalArgumentException(errorMsg);
    }
    return result;
  }

  /**
   * Convert yyyyq int format to Yqq String format throwing exception if format is incorrect
   *
   * @param yyyyq int (e.g. 20194 for Fall 2019
   * @throws IllegalArgumentException
   * @return Qyy representation (e.g. F19)
   */
  public static String yyyyqToQyy(int yyyyq) {
    if (invalidQtr(yyyyq)) {
      throw new IllegalArgumentException(
          "Argument should be a five digit integer in qqqqy format ending in 1,2,3 or 4");
    }
    return String.format("%s%s", getQ(yyyyq), getYY(yyyyq));
  }

  /**
   * Take yyyyq int format and return single character for quarter, either "W", "S", "M", or "F" for
   * last digit 1, 2, 3, 4, respectively. Throw illegal argument exception if not in yyyyq format.
   *
   * @param yyyyq int (e.g. 20194 for Fall 2019)
   * @throws IllegalArgumentException
   * @return single char string for quarter (e.g. "F")
   */
  public static String getQ(int yyyyq) {
    if (invalidQtr(yyyyq)) {
      throw new IllegalArgumentException(
          "Argument should be a five digit integer in qqqqy format ending in 1,2,3 or 4");
    }
    String[] quarters = new String[] {"W", "S", "M", "F"};
    int index = yyyyq % 10;
    return quarters[index - 1];
  }

  /**
   * Take yyyyq int format and return two digit year as a String Throw illegal argument exception if
   * not in yyyyq format.
   *
   * @param yyyyq int (e.g. 20194 for Fall 2019)
   * @throws IllegalArgumentException
   * @return two char string for year (e.g. "19")
   */
  public static String getYY(int yyyyq) {
    if (invalidQtr(yyyyq)) {
      throw new IllegalArgumentException(
          "Argument should be a five digit integer in qqqqy format ending in 1,2,3 or 4");
    }
    return String.format("%02d", (yyyyq / 10) % 100);
  }

  /**
   * Take yyyyq int format and return four digit year as a String Throw illegal argument exception
   * if not in yyyyq format.
   *
   * @param yyyyq int (e.g. 20194 for Fall 2019)
   * @throws IllegalArgumentException
   * @return four char string for year (e.g. "2019")
   */
  public static String getYYYY(int yyyyq) {
    if (invalidQtr(yyyyq)) {
      throw new IllegalArgumentException(
          "Argument should be a five digit integer in qqqqy format ending in 1,2,3 or 4");
    }
    return String.format("%04d", (yyyyq / 10));
  }

  public static int qyyToyyyyQ(String qyy) {
    if (qyy.length() != 3) throw new IllegalArgumentException("Argument should be in QYY format");

    char q = qyy.charAt(0);
    String yy = qyy.substring(1, 3);
    String legalQuarters = "WSMF";
    int qInt = legalQuarters.indexOf(q) + 1;
    if (invalidQtr(qInt)) {
      throw new IllegalArgumentException("First char should be one of " + legalQuarters);
    }
    int yyInt = Integer.parseInt(yy);
    int century = (yyInt > 50) ? 1900 : 2000;
    return (century + yyInt) * 10 + qInt;
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof Quarter)) {
      return false;
    }
    Quarter quarter = (Quarter) o;
    return yyyyq == quarter.yyyyq;
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(yyyyq);
  }

  /**
   * return a list of Quarters starting with the start parameter and ending with the end parameter,
   * inclusive.
   *
   * <p>The result will automatically go in chronological or reverse chronological order, depending
   * on the order of the parameters.
   *
   * @param start
   * @param end
   * @return list of quarters in specified order
   */
  public static List<Quarter> quarterList(String start, String end) {
    List<Quarter> result = new ArrayList<Quarter>();

    int startInt = Quarter.yyyyqToInt(start);
    int endInt = Quarter.yyyyqToInt(end);

    if (startInt < endInt) {
      for (Quarter iter = new Quarter(startInt); iter.getValue() <= endInt; iter.increment()) {
        Quarter q = new Quarter(iter.getValue());
        result.add(q);
      }
    }
    if (startInt >= endInt) {
      for (Quarter iter = new Quarter(startInt); iter.getValue() >= endInt; iter.decrement()) {
        Quarter q = new Quarter(iter.getValue());
        result.add(q);
      }
    }
    return result;
  }
}