flightrules

package
v1.3.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

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

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

func DisplayPICName(f *models.Flight, userName string) string

DisplayPICName returns the value to render in the "PIC Name" column of a logbook row, applying the canonical fallback chain:

  1. 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)
  2. flight.InstructorName, if set (legacy column; pre-crew-table flights)
  3. 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)
  4. first non-self Examiner in flight.CrewMembers (an exam flight is Dual and the examiner is PIC of record)
  5. original PICName if present (e.g. "Self" with no instructor available)
  6. "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

func EffectiveIFRTime(f *models.Flight) int

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

func ExaminerNameFromCrew(f *models.Flight, userName string) string

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

func InstructorNameFromCrew(f *models.Flight, userName string) string

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

func IsFSTDRow(f *models.Flight) bool

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

func IsMultiPilotOperation(flight *models.Flight, role Role) bool

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

func IsNightAt(t time.Time, lat, lon float64) bool

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

func MatchesUser(candidate, userName string) bool

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

func NormalizeLegacyRole(s string) string

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

func NormalizeName(s string) string

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

func PICNameFromCrew(f *models.Flight, userName string) string

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

func ResolvePICNameForSave(f *models.Flight, userName string) *string

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.

func RowTimes

func RowTimes(f *models.Flight, acClass string) (spSE, spME, mp int)

RowTimes returns the minutes that should be filled into the SP-SE / SP-ME / MP columns for `flight` given its piloting category. Exactly one of the three returned values is non-zero (the others are 0).

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

type LegacyCrewMember struct {
	Name string
	Role string
}

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

func DetermineRole(flight *models.Flight, userName string) Role

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL