Documentation
¶
Overview ¶
Package flightrules is the single source of truth for "who/what counts as PIC, Dual, FI, Night, IFR, MP/SP …" across the codebase.
flightcalc/ owns the *write* path (mutates Flight on save). flightrules owns the *read* path (pure helpers consumed by handlers, exporters, PDF renderers, stats and tests). The two share the role + name primitives below so they cannot disagree.
IMPORTANT: do NOT inline any of these rules in handlers/, repository/ or exporter code. A grep-guard in scripts/run-all-tests.sh enforces this.
Index ¶
- func CombinedRemarks(f *models.Flight, flags ...RemarkFlag) string
- func DisplayPICName(f *models.Flight, userName string) string
- func EffectiveIFRTime(f *models.Flight) int
- func ExaminerNameFromCrew(f *models.Flight, userName string) string
- func FSTDFields(f *models.Flight, dateLayout string, timeFmt func(int) string) (date, kind, dur string)
- func InstructorNameFromCrew(f *models.Flight, userName string) string
- func IsFSTDRow(f *models.Flight) bool
- func IsMultiPilotOperation(flight *models.Flight, role Role) bool
- func IsNightAt(t time.Time, lat, lon float64) bool
- func MatchesUser(candidate, userName string) bool
- func NormalizeLegacyRole(s string) string
- func NormalizeName(s string) string
- func PICNameFromCrew(f *models.Flight, userName string) string
- func ResolvePICNameForSave(f *models.Flight, userName string) *string
- func RowTimes(f *models.Flight, acClass string) (spSE, spME, mp int)
- type LegacyCrewInput
- type LegacyCrewMember
- type PilotingCategory
- type RemarkFlag
- type Role
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CombinedRemarks ¶
func CombinedRemarks(f *models.Flight, flags ...RemarkFlag) string
CombinedRemarks returns the user-facing combined Remarks + Endorsements string, optionally suffixed with FAA-style inline flags ([IPC] / [FR] / [PC]). Empty endorsements/remarks are skipped; the separator between a non-empty remark and a non-empty endorsement is " | ".
Flag suffixes are only added when the corresponding boolean on `flight` is true AND the flag was requested by the caller. Most callers pass all three flags and let the model state decide.
func DisplayPICName ¶
DisplayPICName returns the value to render in the "PIC Name" column of a logbook row, applying the canonical fallback chain:
- flight.PICName, if set (but stale "Self" on a Dual flight is treated as unset so the instructor — who is PIC of record on a Dual — wins)
- flight.InstructorName, if set (legacy column; pre-crew-table flights)
- first non-self Instructor in flight.CrewMembers (modern data shape: the FE only writes the instructor into CrewMembers, leaving InstructorName nil — the instructor *is* PIC of record on a Dual)
- first non-self Examiner in flight.CrewMembers (an exam flight is Dual and the examiner is PIC of record)
- original PICName if present (e.g. "Self" with no instructor available)
- "SELF"
userName is the authenticated user's display name; pass "" if unknown. All exporters (CSV, PDF, future formats) MUST use this helper instead of re-implementing the fallback locally.
func EffectiveIFRTime ¶
EffectiveIFRTime returns the IFR/Instrument time minutes that should be reported for a flight. If the user explicitly set IFRTime it is returned as-is. Otherwise the sum of ActualInstrumentTime + SimulatedInstrumentTime is used (capped at TotalTime so a logging mistake cannot produce >100%).
This is the single source of truth for IFR derivation: imports, the auto-calc pipeline and exporters all flow through it.
func ExaminerNameFromCrew ¶ added in v1.3.0
ExaminerNameFromCrew is the Examiner-role counterpart of InstructorNameFromCrew: the first third-party Examiner in the crew, or "".
func FSTDFields ¶
func FSTDFields(f *models.Flight, dateLayout string, timeFmt func(int) string) (date, kind, dur string)
FSTDFields returns the (date, type, time) triple to fill into the FSTD columns. dateLayout is the Go time layout used to format the date (e.g. "02.01.2006" for full EASA CSV, "02.01" for the compact PDF column). timeFmt formats the minutes value the way the caller wants (HH:MM, decimal hours, etc.).
Returns ("", "", "") when IsFSTDRow is false.
func InstructorNameFromCrew ¶
InstructorNameFromCrew returns the name of the first CrewMember with the Instructor role whose name does NOT match userName (i.e. a third-party instructor). Returns "" if no such member exists or if `f` is nil.
userName is the authenticated user's display name and may be empty; in that case ALL Instructor crew members are treated as third parties (matches the conservative behaviour of DetermineRole).
func IsFSTDRow ¶
IsFSTDRow reports whether a flight row should populate the FSTD-block columns (EASA AMC1 FCL.050 Cols 20-22, FAA §61.51(b)(1)(iv)).
Both: a non-empty FSTDType identifier AND non-zero simulated flight time.
func IsMultiPilotOperation ¶ added in v1.3.0
IsMultiPilotOperation reports whether the crew composition indicates an operation flown with a two-pilot crew (multi-crew cooperation): the user is the co-pilot (RoleSIC), or a crew member holds the SIC role (the user is PIC with a co-pilot). Per AMC1 FCL.050 both pilots then log the full flight time in the multi-pilot column.
func IsNightAt ¶
IsNightAt is the single point of truth for "is this instant night?" at a given latitude/longitude. It delegates to pkg/solar but every caller (flightcalc takeoff/landing splits, night-minute walk, future stats) goes through here so we can swap in civil-twilight or AMC FCL.010 rules in one place if the spec is ever clarified.
func MatchesUser ¶
MatchesUser reports whether `candidate` refers to the same person as `userName`. Comparison is case- and whitespace-insensitive, and also recognises the "Last, First" contact-picker form against a stored "First Last" profile (and vice versa).
Examples (all match "Amelia Earhart"):
- "Amelia Earhart", "amelia earhart", "AMELIA EARHART"
- " Amelia Earhart ", "Amelia\u00a0Earhart"
- "Earhart, Amelia"
Examples (do NOT match): "Amelia M. Earhart", "A. Earhart".
func NormalizeLegacyRole ¶ added in v1.2.3
NormalizeLegacyRole maps a free-form role string (case-insensitive) to one of our canonical CrewRole values. Unknown / empty strings return "" to signal "fall back to positional inference".
ForeFlight's role vocabulary in the structured Person cell includes PIC, SIC, Student, Instructor, FlightAttendant, Observer, Engineer and Other; the latter four are flattened to Passenger.
func NormalizeName ¶
NormalizeName returns a comparable form of a person name: lower-case, trimmed of leading/trailing whitespace, with all runs of internal whitespace collapsed to a single ASCII space, and Unicode whitespace (e.g. U+00A0 non-breaking space, tabs, newlines) folded to ASCII space.
This is the only place name normalization should happen. Empty input yields empty output.
func PICNameFromCrew ¶ added in v1.3.0
PICNameFromCrew is the PIC-role counterpart of InstructorNameFromCrew: the first third-party PIC in the crew, or "". Used when the user flies as co-pilot (SIC) — the other pilot is PIC of record.
func ResolvePICNameForSave ¶
ResolvePICNameForSave returns the value that should be persisted into `flight.PICName` when a flight is saved. The write-path equivalent of DisplayPICName:
- existing PICName wins (user explicitly set it),
- if the user is PIC and no instructor is involved → "Self",
- if the user is Dual and an InstructorName is set → that instructor,
- if the user is Dual and a non-self Instructor exists in CrewMembers → that instructor's name (modern data shape),
- if the user is Dual and a non-self Examiner exists in CrewMembers → that examiner's name (exam flight: the examiner is PIC of record),
- otherwise nil (column stays empty; exporter will fall through to DisplayPICName's "SELF" default at render time).
The CRUD handler must call this once at save time so the persisted column is canonical.
Types ¶
type LegacyCrewInput ¶
type LegacyCrewInput struct {
Person1, Person2, Person3, Person4, Person5, Person6 string
Person1Role, Person2Role, Person3Role, Person4Role, Person5Role, Person6Role string
InstructorName string
HasDualReceived bool // any positive DualReceived value
HasDualGiven bool // any positive DualGiven value
}
LegacyCrewInput describes the flat set of legacy/spreadsheet columns we historically used to encode crew before the CrewMembers array existed: up to six "personN" names, an InstructorName, and the user-declared DualReceived / DualGiven totals.
ForeFlight optionally encodes a role tag inside each Person cell ("Name;Role;email"). When that explicit role is available the caller SHOULD pass it via the matching PersonNRole field so positional inference does not overwrite the source-of-truth tag (e.g. a flight where the instructor is Person2 and Person1 is the student).
type LegacyCrewMember ¶
LegacyCrewMember is a name+role pair returned by InferLegacyCrew. Callers map it to their own crew type (generated.FlightCrewMemberInput, models.FlightCrewMember, …). Role values match the strings in models.CrewRole ("PIC", "SIC", "Instructor", "Student", "Passenger").
func InferLegacyCrew ¶
func InferLegacyCrew(in LegacyCrewInput) []LegacyCrewMember
InferLegacyCrew is the single source of truth that converts the legacy flat crew columns (used by ForeFlight / Garmin / older spreadsheet CSVs) into a CrewMembers array. Both the CSV importer and any future legacy re-importer MUST use this — do not re-derive crew roles by hand.
Rules (preserved exactly from the historical importer):
- Training flight ≡ InstructorName set OR HasDualReceived.
- Instructor-giving ≡ HasDualGiven (and not a training flight).
- Person1 is treated as Instructor on a training flight, Student when the user is giving instruction, else PIC. If InstructorName is set and differs from Person1, both are emitted (Person1 keeps a PIC role, InstructorName is added as Instructor).
- Person2 is Student on a training flight, Passenger otherwise.
- Persons 3-6 are always Passenger.
- InstructorName with no Person1 is emitted as Instructor on its own.
When the caller supplies an explicit role for a person via the matching PersonNRole field (see NormalizeLegacyRole for the accepted vocabulary), that role wins over the positional rules for that person. Other persons still follow the positional rules so a partial set of tags works (e.g. only Person1 tagged "Student", Person2 left untagged still becomes the Passenger / Student per the positional rule).
type PilotingCategory ¶
type PilotingCategory int
PilotingCategory is the per-row bucket used for the EASA AMC1 FCL.050 columns "SP-SE" / "SP-ME" / "Multi-Pilot".
const ( // CategorySPSE: single-pilot single-engine. CategorySPSE PilotingCategory = iota // CategorySPME: single-pilot multi-engine. CategorySPME // CategoryMP: multi-pilot. CategoryMP )
func PilotingCategoryFor ¶
func PilotingCategoryFor(f *models.Flight, acClass string) PilotingCategory
PilotingCategoryFor returns the bucket for `flight`. acClass is the user's stored AircraftClass for this registration (e.g. "SEP", "MEP", "SET", "MET", "SES"), passed in by callers that have aircraft data. When acClass is empty (no fleet entry available) the rule defaults to SP-SE.
Rule:
- MultiPilotTime > 0 ⇒ MP (user-declared)
- acClass starts with "ME" or "MET" or "SET" ⇒ SP-ME
- otherwise ⇒ SP-SE
type RemarkFlag ¶
type RemarkFlag int
RemarkFlag is an optional inline marker appended to a flight's remarks when present (FAA logbook convention). Order matches FAA convention: [IPC], then [FR] (Flight Review), then [PC] (Proficiency Check).
const ( FlagIPC RemarkFlag = iota FlagFlightReview FlagProficiencyCheck )
type Role ¶
type Role int
Role classifies the user's pilot role on a flight, derived from the crew composition relative to the authenticated user's display name.
const ( // RolePIC: user is sole/lead pilot, no instruction context. RolePIC Role = iota // RoleDualReceiving: a third-party Instructor on board is giving the // user instruction, or a third-party Examiner is conducting a check // ride (Dual received). RoleDualReceiving // RoleDualGiving: the user is acting as instructor — either a Student // is on board, or the user themselves is listed with the Instructor // role (Dual given / FI). RoleDualGiving // RoleSIC: the user is co-pilot on a multi-pilot operation — another // person is listed with the PIC role, or the user themselves is listed // with the SIC role. Per AMC1 FCL.050 only the designated PIC logs PIC // time; the co-pilot logs co-pilot (SIC) time, even if both pilots are // qualified PICs (FOCA GM/INFO "Logging of Flight Time" §2.3.3). RoleSIC )
func DetermineRole ¶
DetermineRole inspects the crew list to classify the user's pilot role.
Precedence: a third-party Instructor or Examiner (name ≠ user) makes the user a Dual receiver, regardless of any Student also being present (e.g. observed CFI check rides). A Student or self-listed Instructor makes the user a Dual giver. A third-party PIC or a self-listed SIC makes the user the co-pilot (SIC) of a multi-pilot operation. Otherwise the user is PIC.
A third-party Examiner counts as Dual received because there can only be one PIC per flight and the examiner occupying a pilot seat is PIC of record (NfL 2021-2-602 §4.2.2 no. 4; EASA AMC1 FCL.050). A self-listed Examiner leaves the user as PIC — an examiner logs their exam flights as PIC time.
A third-party PIC counts as SIC for the same "only one PIC per flight" reason: if someone else was designated PIC, the user occupying the other pilot seat logs co-pilot time (AMC1 FCL.050; FOCA GM/INFO §2.3.3), even when both pilots hold PIC qualifications. A self-listed PIC crew entry keeps the user as PIC and wins over a simultaneous third-party PIC entry (conflicting data — trust the user's explicit self-declaration).
When userName is empty, any Instructor, Examiner or PIC crew member is conservatively treated as a third party, preserving prior behaviour for callers that do not yet have user context.