@Override
  public void exitNthCenturyRange(NthCenturyRangeContext ctx) {
    if (ctx.exception != null) return;

    Era era = (Era) stack.pop();
    Integer endN = (Integer) stack.pop();
    Part endPart = (Part) stack.pop();
    Integer startN = (Integer) stack.pop();
    Part startPart = (Part) stack.pop();

    if (era == null) {
      era = Date.DEFAULT_ERA;
    }

    int startYear = DateUtils.nthCenturyToYear(startN);
    int endYear = DateUtils.nthCenturyToYear(endN);

    stack.push(
        startPart == null
            ? DateUtils.getCenturyStartDate(startYear, era)
            : DateUtils.getPartialCenturyStartDate(startYear, startPart, era));
    stack.push(
        startPart == null
            ? DateUtils.getCenturyEndDate(startYear, era)
            : DateUtils.getPartialCenturyEndDate(startYear, startPart, era));
    stack.push(
        endPart == null
            ? DateUtils.getCenturyStartDate(endYear, era)
            : DateUtils.getPartialCenturyStartDate(endYear, endPart, era));
    stack.push(
        endPart == null
            ? DateUtils.getCenturyEndDate(endYear, era)
            : DateUtils.getPartialCenturyEndDate(endYear, endPart, era));
  }
  @Override
  public void exitMillennium(MillenniumContext ctx) {
    if (ctx.exception != null) return;

    Era era = (Era) stack.pop();
    Integer n = (Integer) stack.pop();

    if (era != null) {
      // If the era was explicitly specified, the start and end years
      // may be calculated now.

      stack.push(DateUtils.getMillenniumStartDate(n, era));
      stack.push(DateUtils.getMillenniumEndDate(n, era));
    } else {
      // If the era was not explicitly specified, the start and end years
      // can't be calculated yet. The calculation must be deferred until
      // later. For example, this millennium may be the start of a hyphenated
      // range, where the era will be inherited from the era of the end of
      // the range; this era won't be known until farther up the parse tree,
      // when both sides of the range will have been parsed.

      stack.push(new DeferredMillenniumStartDate(n));
      stack.push(new DeferredMillenniumEndDate(n));
    }
  }
  @Override
  public void exitUncertainDate(UncertainDateContext ctx) {
    if (ctx.exception != null) return;

    Date latestDate = (Date) stack.pop();
    Date earliestDate = (Date) stack.pop();

    int earliestInterval =
        DateUtils.getCircaIntervalYears(earliestDate.getYear(), earliestDate.getEra());
    int latestInterval = DateUtils.getCircaIntervalYears(latestDate.getYear(), latestDate.getEra());

    // Express the circa interval as a qualifier.

    // stack.push(earliestDate.withQualifier(QualifierType.MINUS, earliestInterval,
    // QualifierUnit.YEARS));
    // stack.push(latestDate.withQualifier(QualifierType.PLUS, latestInterval,
    // QualifierUnit.YEARS));

    // OR:

    // Express the circa interval as an offset calculated into the year.

    DateUtils.subtractYears(earliestDate, earliestInterval);
    DateUtils.addYears(latestDate, latestInterval);

    stack.push(earliestDate);
    stack.push(latestDate);
  }
  @Override
  public void exitHalfYear(HalfYearContext ctx) {
    if (ctx.exception != null) return;

    Era era = (Era) stack.pop();
    Integer year = (Integer) stack.pop();
    Integer half = (Integer) stack.pop();

    stack.push(DateUtils.getHalfYearStartDate(half, year).withEra(era));
    stack.push(DateUtils.getHalfYearEndDate(half, year, era).withEra(era));
  }
  @Override
  public void exitPartialYear(PartialYearContext ctx) {
    if (ctx.exception != null) return;

    Era era = (Era) stack.pop();
    Integer year = (Integer) stack.pop();
    Part part = (Part) stack.pop();

    stack.push(DateUtils.getPartialYearStartDate(part, year).withEra(era));
    stack.push(DateUtils.getPartialYearEndDate(part, year, era).withEra(era));
  }
  @Override
  public void exitQuarterInYearRange(QuarterInYearRangeContext ctx) {
    if (ctx.exception != null) return;

    Era era = (Era) stack.pop();
    Integer year = (Integer) stack.pop();
    Integer lastQuarter = (Integer) stack.pop();
    Integer firstQuarter = (Integer) stack.pop();

    stack.push(DateUtils.getQuarterYearStartDate(firstQuarter, year).withEra(era));
    stack.push(DateUtils.getQuarterYearEndDate(firstQuarter, year, era).withEra(era));
    stack.push(DateUtils.getQuarterYearStartDate(lastQuarter, year).withEra(era));
    stack.push(DateUtils.getQuarterYearEndDate(lastQuarter, year, era).withEra(era));
  }
  @Override
  public void exitMonthInYearRange(MonthInYearRangeContext ctx) {
    if (ctx.exception != null) return;

    Era era = (Era) stack.pop();
    Integer year = (Integer) stack.pop();
    Integer numMonthEnd = (Integer) stack.pop();
    Integer numMonthStart = (Integer) stack.pop();

    stack.push(new Date(year, numMonthStart, 1, era));
    stack.push(
        new Date(year, numMonthStart, DateUtils.getDaysInMonth(numMonthStart, year, era), era));
    stack.push(new Date(year, numMonthEnd, 1, era));
    stack.push(new Date(year, numMonthEnd, DateUtils.getDaysInMonth(numMonthEnd, year, era), era));
  }
  @Override
  public void exitYearSpanningWinter(YearSpanningWinterContext ctx) {
    if (ctx.exception != null) return;

    Era era = (Era) stack.pop();
    Integer endYear = (Integer) stack.pop();
    Integer startYear = (Integer) stack.pop();

    stack.push(new Date(startYear, 12, 1).withEra(era));
    stack.push(DateUtils.getQuarterYearEndDate(1, endYear, era).withEra(era));
  }
  @Override
  public void exitMonth(MonthContext ctx) {
    if (ctx.exception != null) return;

    Era era = (Era) stack.pop();
    Integer year = (Integer) stack.pop();
    Integer numMonth = (Integer) stack.pop();

    stack.push(new Date(year, numMonth, 1, era));
    stack.push(new Date(year, numMonth, DateUtils.getDaysInMonth(numMonth, year, era), era));
  }
  @Override
  public void exitBeforeOrAfterDate(BeforeOrAfterDateContext ctx) {
    if (ctx.exception != null) return;

    Date latestDate = (Date) stack.pop();
    Date earliestDate = (Date) stack.pop();

    // Set null eras to the default.

    if (earliestDate.getEra() == null) {
      earliestDate.setEra(Date.DEFAULT_ERA);
    }

    if (latestDate.getEra() == null) {
      latestDate.setEra(Date.DEFAULT_ERA);
    }

    // Finalize any deferred calculations.

    if (latestDate instanceof DeferredDate) {
      ((DeferredDate) latestDate).resolveDate();
    }

    if (earliestDate instanceof DeferredDate) {
      ((DeferredDate) earliestDate).resolveDate();
    }

    // Calculate the earliest date or end date.

    if (ctx.BEFORE() != null) {
      latestDate = earliestDate;
      earliestDate = DateUtils.getEarliestBeforeDate(earliestDate, latestDate);
    } else if (ctx.AFTER() != null) {
      earliestDate = latestDate;
      latestDate = DateUtils.getLatestAfterDate(earliestDate, latestDate);
    }

    stack.push(earliestDate);
    stack.push(latestDate);
  }
  @Override
  public void exitStrCentury(StrCenturyContext ctx) {
    if (ctx.exception != null) return;

    Integer n = (Integer) stack.pop();

    // Convert the nth number to a year number,
    // and push on the stack.

    Integer year = DateUtils.nthCenturyToYear(n);

    stack.push(year);
  }
  @Override
  public void exitNumDate(NumDateContext ctx) {
    if (ctx.exception != null) return;

    // This could either be year-month-day, or
    // month-day-year. Try to determine which,
    // and reorder the stack into the canonical
    // year-month-day-era ordering.

    Era era = (Era) stack.pop();
    Integer num3 = (Integer) stack.pop();
    Integer num2 = (Integer) stack.pop();
    Integer num1 = (Integer) stack.pop();

    // Default to a year-month-day interpretation.

    int year = num1;
    int numMonth = num2;
    int dayOfMonth = num3;

    if (DateUtils.isValidDate(num1, num2, num3, era)) {
      // Interpreting as year-month-day produces a valid date. Go with it.
    } else if (DateUtils.isValidDate(num3, num1, num2, era)) {
      // Interpreting as year-month-day doesn't produce a valid date, but
      // month-day-year does. Go with month-day-year.

      year = num3;
      numMonth = num1;
      dayOfMonth = num2;
    }

    stack.push(year);
    stack.push(numMonth);
    stack.push(dayOfMonth);
    stack.push(era);
  }
  @Override
  public void exitStrMonth(StrMonthContext ctx) {
    if (ctx.exception != null) return;

    // Convert the month name to a number,
    // and push on the stack.

    TerminalNode monthNode = ctx.MONTH();

    if (monthNode == null) {
      monthNode = ctx.SHORTMONTH();
    }

    String monthStr = monthNode.getText();

    stack.push(DateUtils.getMonthByName(monthStr));
  }