public override void Finalize(decimal totalTime, decimal picTime) { // compute the expiration date. if (!HasBeenCurrent) { return; } // see when last check would have expired // we have to go through each of the ones we have seen, due to 61.58(i)'s ridiculous rules // So, for example, suppose we see the following dates: // June 8, 2012 - good until June 30, 2013 // June 12, 2013 - good until June 30, 2014 // July 9, 2014 - ONLY GOOD until June 30 (since this is in the month after it is due // May 28, 2015 - GOOD UNTIL June 30 2015 (since this is in the month before it was due) // Aug 8, 2016 - Good until Aug 31, 2017 lstDatesOfCurrencyChecks.Sort(); // need to go in ascending order. DateTime effectiveLastCheck = lstDatesOfCurrencyChecks[0]; foreach (DateTime dt in lstDatesOfCurrencyChecks) { DateTime dtBracketStart = effectiveLastCheck.Date.AddCalendarMonths(ExpirationSpan - 2).AddDays(1); // first day of the calendar month preceding expiration DateTime dtBracketEnd = effectiveLastCheck.Date.AddCalendarMonths(ExpirationSpan + 1); // last day of the month following expiration if (dtBracketStart.CompareTo(dt.Date) <= 0 && dtBracketEnd.CompareTo(dt.Date) >= 0) // fell in the bracket { effectiveLastCheck = effectiveLastCheck.AddCalendarMonths(ExpirationSpan); // so go ExpirationSpan from that prior check } else { effectiveLastCheck = dt; } } dtExpiration = effectiveLastCheck.AddCalendarMonths(ExpirationSpan); }
private static void AddFAA2ndClassItems(List <CurrencyStatusItem> lst, DateTime lastMedical, bool fWas40AtExam) { CurrencyStatusItem cs = StatusForDate(lastMedical.AddCalendarMonths(12), Resources.Currency.NextMedical2ndClass, CurrencyStatusItem.CurrencyGroups.Medical); lst.Add(cs); if (cs.Status == CurrencyState.NotCurrent) { lst.Add(StatusForDate(lastMedical.AddCalendarMonths(fWas40AtExam ? 24 : 60), Resources.Currency.NextMedical3rdClassPrivs, CurrencyStatusItem.CurrencyGroups.Medical)); } }
private static void AddCanadaCommercialItems(List <CurrencyStatusItem> lst, DateTime lastMedical, bool fWas40AtExam, bool fWas60AtExam) { // Canadian rules: https://laws-lois.justice.gc.ca/eng/regulations/sor-96-433/page-36.html#h-991075 and https://tc.canada.ca/sites/default/files/2021-09/AIM-2021-2_LRA-E.pdf // 12 months for commercial/ATP, unless (a) over 60, or (b) single-pilot and over 40 lst.Add(StatusForDate(lastMedical.AddCalendarMonths(fWas60AtExam ? 6 : 12), Resources.Currency.NextMedicalCanadaCommercial, CurrencyStatusItem.CurrencyGroups.Medical)); if (fWas40AtExam && !fWas60AtExam) { lst.Add(StatusForDate(lastMedical.AddCalendarMonths(6), Resources.Currency.NextMedicalCanadaSinglePilot, CurrencyStatusItem.CurrencyGroups.Medical)); } lst.Add(StatusForDate(lastMedical.AddCalendarMonths(fWas40AtExam ? 24 : 60), Resources.Currency.NextMedicalCanadaPPL, CurrencyStatusItem.CurrencyGroups.Medical)); }
private static void AddFAA1stClassItems(List <CurrencyStatusItem> lst, DateTime lastMedical, bool fWas40AtExam) { CurrencyStatusItem cs = StatusForDate(lastMedical.AddCalendarMonths(fWas40AtExam ? 6 : 12), Resources.Currency.NextMedical1stClass, CurrencyStatusItem.CurrencyGroups.Medical); lst.Add(cs); if (cs.Status == CurrencyState.NotCurrent) { if (fWas40AtExam) // may still have some non-ATP commercial time left { lst.Add(StatusForDate(lastMedical.AddCalendarMonths(12), Resources.Currency.NextMedical1stClassCommercial, CurrencyStatusItem.CurrencyGroups.Medical)); } lst.Add(StatusForDate(lastMedical.AddCalendarMonths(fWas40AtExam ? 24 : 60), Resources.Currency.NextMedical3rdClassPrivs, CurrencyStatusItem.CurrencyGroups.Medical)); } }
/// <summary> /// Computes the overall currency /// </summary> public void RefreshCurrency() { // Compute currency according to 61.57(c)(6) ( => (c)(3)) (i) and (ii) including each one's expiration. if (m_fCacheValid) { return; } // 61.57(c)(6)(i) => (c)(3)(i) - no passengers. IPC covers this. FlightCurrency fc6157c6i = fcGliderIFRTime.AND(fcGliderInstrumentManeuvers).OR(fcGliderIPC); // 61.57(c)(6)(ii) => (c)(3)(ii) - passengers. Above + two more. Again, IPC covers this too. FlightCurrency fc6157c6ii = fc6157c6i.AND(fcGliderIFRTimePassengers).AND(fcGliderInstrumentPassengers).OR(fcGliderIPC); m_fCurrentSolo = fc6157c6i.IsCurrent(); m_fCurrentPassengers = fc6157c6ii.IsCurrent(); if (m_fCurrentSolo || m_fCurrentPassengers) { if (m_fCurrentPassengers) { m_csCurrent = fc6157c6ii.CurrentState; m_dtExpiration = fc6157c6ii.ExpirationDate; m_szDiscrepancy = fc6157c6ii.DiscrepancyString; } else // must be current solo) { m_csCurrent = fc6157c6i.CurrentState; m_dtExpiration = fc6157c6i.ExpirationDate; m_szDiscrepancy = fc6157c6i.DiscrepancyString; } // check to see if there is an embedded gap that needs an IPC CurrencyPeriod[] rgcpMissingIPC = fc6157c6i.FindCurrencyGap(fcGliderIPC.MostRecentEventDate, 6); if (rgcpMissingIPC != null && m_szDiscrepancy.Length == 0) { m_szDiscrepancy = String.Format(CultureInfo.CurrentCulture, Resources.Currency.IPCMayBeRequired, rgcpMissingIPC[0].EndDate.ToShortDateString(), rgcpMissingIPC[1].StartDate.ToShortDateString()); } } else { // And expiration date is the later of the passenger/no-passenger date m_dtExpiration = fc6157c6i.ExpirationDate.LaterDate(fc6157c6ii.ExpirationDate); // see if we need an IPC // if more than 6 calendar months have passed since expiration, an IPC is required. // otherwise, just name the one that is short if (DateTime.Compare(m_dtExpiration.AddCalendarMonths(6).Date, DateTime.Now.Date) > 0) { m_szDiscrepancy = String.Format(CultureInfo.CurrentCulture, Resources.Currency.DiscrepancyTemplateGliderIFRPassengers, fcGliderIFRTime.Discrepancy, fcGliderInstrumentManeuvers.Discrepancy); } else { m_szDiscrepancy = Resources.Currency.IPCRequired; } } m_fCacheValid = true; }
private static void AddOtherMedicalItems(List <CurrencyStatusItem> lst, DateTime lastMedical, int monthsToMedical, bool fUsesICAOMedical) { if (monthsToMedical > 0) { lst.Add(StatusForDate(fUsesICAOMedical ? lastMedical.AddMonths(monthsToMedical) : lastMedical.AddCalendarMonths(monthsToMedical), Resources.Currency.NextMedical, CurrencyStatusItem.CurrencyGroups.Medical)); } }
/// <summary> /// Computes a new due date based on the passed-in date and the regen rules /// </summary> /// <param name="dt">The date the regen is requested</param> /// <returns>The new due date</returns> public DateTime NewDueDateBasedOnDate(DateTime dt) { if (!dt.HasValue()) { dt = Expiration; } switch (RegenType) { default: case RegenUnit.None: return(dt); case RegenUnit.Days: return(dt.AddDays(RegenSpan)); case RegenUnit.CalendarMonths: return(dt.AddCalendarMonths(RegenSpan)); case RegenUnit.Hours: throw new MyFlightbookException("Deadline is hour based, not date based!"); } }
/// <summary> /// Returns an enumerable of medical status based on https://www.law.cornell.edu/cfr/text/14/61.23 for FAA medicals, or else expiration and type /// </summary> /// <returns></returns> public static IEnumerable <CurrencyStatusItem> MedicalStatus(DateTime lastMedical, int monthsToMedical, MedicalType mt, DateTime?dob, bool fUsesICAOMedical) { // if no last medical, then this is easy: we know nothing. if (!lastMedical.HasValue()) { return(Array.Empty <CurrencyStatusItem>()); } if (RequiresBirthdate(mt) && (dob == null || !dob.HasValue)) { return new CurrencyStatusItem[] { new CurrencyStatusItem(Resources.Currency.NextMedical, Resources.Currency.NextMedicalRequiresBOD, CurrencyState.NotCurrent) { CurrencyGroup = CurrencyStatusItem.CurrencyGroups.Medical } } } ; bool fWas40AtExam = dob != null && dob.Value.AddYears(40).CompareTo(lastMedical) < 0; bool fWas60AtExam = dob != null && dob.Value.AddYears(60).CompareTo(lastMedical) < 0; bool fWas50AtExam = dob != null && dob.Value.AddYears(50).CompareTo(lastMedical) < 0; List <CurrencyStatusItem> lst = new List <CurrencyStatusItem>(); switch (mt) { case MedicalType.Other: AddOtherMedicalItems(lst, lastMedical, monthsToMedical, fUsesICAOMedical); break; case MedicalType.EASA1stClass: AddEASA1stClassItems(lst, lastMedical, fWas40AtExam, fWas60AtExam); break; case MedicalType.EASA2ndClass: AddEASA2ndClassItems(lst, lastMedical, fWas40AtExam, fWas50AtExam, dob.Value); break; case MedicalType.EASALAPL: AddEASALAPLItems(lst, lastMedical, fWas40AtExam, dob.Value); break; case MedicalType.FAA1stClass: AddFAA1stClassItems(lst, lastMedical, fWas40AtExam); break; case MedicalType.FAA2ndClass: AddFAA2ndClassItems(lst, lastMedical, fWas40AtExam); break; case MedicalType.FAA3rdClass: lst.Add(StatusForDate(lastMedical.AddCalendarMonths(fWas40AtExam ? 24 : 60), Resources.Currency.NextMedical, CurrencyStatusItem.CurrencyGroups.Medical)); break; case MedicalType.CASAClass1: AddCASA1stClassItems(lst, lastMedical); break; case MedicalType.CASAClass2: AddCASA2ndClassItems(lst, lastMedical, fWas40AtExam); break; case MedicalType.CanadaPPL: // Canadian rules: https://laws-lois.justice.gc.ca/eng/regulations/sor-96-433/page-36.html#h-991075 and https://tc.canada.ca/sites/default/files/2021-09/AIM-2021-2_LRA-E.pdf // Under 40 years of age: 60 month, Over 40 years of age: 24 month lst.Add(StatusForDate(lastMedical.AddCalendarMonths(fWas40AtExam ? 24 : 60), Resources.Currency.NextMedicalCanadaPPL, CurrencyStatusItem.CurrencyGroups.Medical)); break; case MedicalType.CanadaGlider: // Canadian rules: https://laws-lois.justice.gc.ca/eng/regulations/sor-96-433/page-36.html#h-991075 and https://tc.canada.ca/sites/default/files/2021-09/AIM-2021-2_LRA-E.pdf // Glider or ultralight - 60 months lst.Add(StatusForDate(lastMedical.AddCalendarMonths(60), Resources.Currency.NextMedicalCanadaGlider, CurrencyStatusItem.CurrencyGroups.Medical)); break; case MedicalType.CanadaCommercial: AddCanadaCommercialItems(lst, lastMedical, fWas40AtExam, fWas60AtExam); break; } return(lst); }
/// <summary> /// Predicted date that next BFR is due /// </summary> /// <param name="bfrLast">Date of the last BFR</param> /// <returns>Datetime representing the date of the next BFR, Datetime.minvalue for unknown</returns> public static DateTime NextBFR(DateTime bfrLast) { return(bfrLast.AddCalendarMonths(24)); }
/// <summary> /// Computes the overall currency /// </summary> protected void RefreshCurrency() { // Compute currency according to 61.57(c)(1)-(5) and 61.57(e) // including each one's expiration. The one that expires latest is the expiration date if you are current, the one that // expired most recently is the expiration date since when you were not current. if (m_fCacheValid) { return; } // This is ((61.57(c)(1) OR (2) OR (3) OR (4) OR (5) OR 61.57(e)), each of which is the AND of several currencies. // 61.57(c)(1) (66-HIT in airplane) FlightCurrency fc6157c1 = fcIFRApproach.AND(fcIFRHold); // 61.57(c)(2) (66-HIT in an FTD or flight simulator) FlightCurrency fc6157c2 = fcFTDApproach.AND(fcFTDHold); // 61.57(c)(3) - ATD FlightCurrency fc6157c3 = fcATDHold.AND(fcATDApproach).AND(fcUnusualAttitudesAsc).AND(fcUnusualAttitudesDesc).AND(fcInstrumentHours); // 61.57(c)(4) - Combo STRICT - 6 approaches/hold in an ATD PLUS an approach/hold in an airplane PLUS an approach/hold in an FTD FlightCurrency fc6157c4 = fcATDAppch6Month.AND(fcATDHold6Month).AND(fcIFRHold).AND(fcAirplaneApproach6Month).AND(fcFTDApproach6Month).AND(fcFTDHold); // 61.57(c)(4) - Combo LOOSE - any combination that yields 66-HIT, but require at least one aircraft approach and hold. // FlightCurrency fc6157c4Loose = fcComboApproach6Month.AND(fcAirplaneApproach6Month).AND(fcIFRHold.OR(fcATDHold6Month).OR(fcFTDHold)); // 61.57(c)(5) - combo meal, but seems redundant with (c)(2)/(3). I.e., if you've met this, you've met (2) or (3). // 61.57 (e) - IPC; no need to AND anything together for this one. FlightCurrency fcIFR = fc6157c1.OR(fc6157c2).OR(fc6157c3).OR(fc6157c4).OR(fcIPCOrCheckride); /* * if (m_fUseLoose6157c4) * fcIFR = fcIFR.OR(fc6157c4Loose); */ m_csCurrent = fcIFR.CurrentState; m_dtExpiration = fcIFR.ExpirationDate; if (fcIPCOrCheckride.IsCurrent() && fcIPCOrCheckride.ExpirationDate.CompareTo(m_dtExpiration) >= 0) { Query.HasHolds = Query.HasApproaches = false; Query.PropertiesConjunction = GroupConjunction.Any; Query.PropertyTypes.Clear(); foreach (CustomPropertyType cpt in CustomPropertyType.GetCustomPropertyTypes()) { if (cpt.IsIPC) { Query.PropertyTypes.Add(cpt); } } } if (m_csCurrent == CurrencyState.NotCurrent) { // if more than 6 calendar months have passed since expiration, an IPC is required. // otherwise, just assume 66-HIT. if (DateTime.Compare(m_dtExpiration.AddCalendarMonths(6).Date, DateTime.Now.Date) > 0) { m_szDiscrepancy = String.Format(CultureInfo.CurrentCulture, Resources.Currency.DiscrepancyTemplateIFR, fcIFRHold.Discrepancy, (fcIFRHold.Discrepancy == 1) ? Resources.Currency.Hold : Resources.Currency.Holds, fcIFRApproach.Discrepancy, (fcIFRApproach.Discrepancy == 1) ? Resources.Totals.Approach : Resources.Totals.Approaches); } else { m_szDiscrepancy = Resources.Currency.IPCRequired; } } else { // Check to see if IPC is required by looking for > 6 month gap in IFR currency. // For now, we won't make you un-current, but we'll warn. // (IPC above, though, is un-current). CurrencyPeriod[] rgcpMissingIPC = fcIFR.FindCurrencyGap(fcIPCOrCheckride.MostRecentEventDate, 6); if (rgcpMissingIPC != null) { m_szDiscrepancy = String.Format(CultureInfo.CurrentCulture, Resources.Currency.IPCMayBeRequired, rgcpMissingIPC[0].EndDate.ToShortDateString(), rgcpMissingIPC[1].StartDate.ToShortDateString()); } else { m_szDiscrepancy = string.Empty; } } m_fCacheValid = true; }