FAA Part 117 Table B FDP Limits as Queryable Data

The exact task this guide solves is turning the §117.13 Table B grid — the ten report-time bands and seven segment columns that set the maximum unaugmented flight duty period — from a printed table into effective-dated rows a rule engine can query in constant time. Table B is the single most-consulted lookup in an FAA compliance system, and inlining its ninety values as nested conditionals is how engines end up with an off-by-one on a band boundary that silently returns the wrong ceiling. This page seeds the whole grid as data, implements the band-and-segment resolver, and pins every cell with assertions. It is the concrete reference behind the regulatory reference tables section and feeds the same numbers the FAA Part 117 rule schema design consumes.

Prerequisites

The full Table B grid

Table B gives the maximum flight duty period in hours for an acclimatised crew member in unaugmented operations, keyed on the scheduled start time (acclimatised) and the number of flight segments.

Start (acclimated) 1 2 3 4 5 6 7+
0000–0359 9 9 9 9 9 9 9
0400–0459 10 10 10 10 9 9 9
0500–0559 12 12 12 12 11.5 11 10.5
0600–0659 13 13 12 12 11.5 11 10.5
0700–1159 14 14 13 13 12.5 12 11.5
1200–1259 13 13 13 13 12.5 12 11.5
1300–1659 12 12 12 12 11.5 11 10.5
1700–2159 12 12 11 11 10 9 9
2200–2259 11 10 10 9 9 9 9
2300–2359 10 10 9 9 9 9 9

Every value is a whole or half hour, so all ninety cells are exact integer minutes — the 11.5-hour cells are 690 minutes, the 12.5-hour cells 750 minutes — which is why the reference layer stores minutes, never float hours.

Step 1 — Seed the grid as rows

Each band becomes a row keyed by its inclusive start and exclusive end, carrying the seven segment values in minutes. Encoding the band edges as time objects lets the resolver compare directly without string parsing.

from datetime import time

# (band_start, band_end_exclusive): [seg1..seg7] in minutes
TABLE_B = {
    (time(0, 0), time(4, 0)):  [540, 540, 540, 540, 540, 540, 540],
    (time(4, 0), time(5, 0)):  [600, 600, 600, 600, 540, 540, 540],
    (time(5, 0), time(6, 0)):  [720, 720, 720, 720, 690, 660, 630],
    (time(6, 0), time(7, 0)):  [780, 780, 720, 720, 690, 660, 630],
    (time(7, 0), time(12, 0)): [840, 840, 780, 780, 750, 720, 690],
    (time(12, 0), time(13, 0)):[780, 780, 780, 780, 750, 720, 690],
    (time(13, 0), time(17, 0)):[720, 720, 720, 720, 690, 660, 630],
    (time(17, 0), time(22, 0)):[720, 720, 660, 660, 600, 540, 540],
    (time(22, 0), time(23, 0)):[660, 600, 600, 540, 540, 540, 540],
    (time(23, 0), time(0, 0)): [600, 600, 540, 540, 540, 540, 540],
}

Verify: the 0700–1159 row reads [840, 840, 780, 780, 750, 720, 690], i.e. 14:00, 14:00, 13:00, 13:00, 12:30, 12:00, 11:30 — the busiest daytime band where the ceiling is highest.

Step 2 — Resolve a band and clamp the segment count

The resolver finds the band covering the report time and reads the segment column, clamping any leg count above seven down to the 7+ column. The final 2300–2359 band wraps midnight, so it matches when the report is at or after 23:00 or before its exclusive end of 00:00.

def resolve_table_b(report: time, segments: int) -> int:
    """Maximum unaugmented FDP in minutes for an acclimatised report and leg count."""
    seg_index = min(max(segments, 1), 7) - 1
    for (lo, hi), minutes in TABLE_B.items():
        covers = (lo <= report < hi) if lo < hi else (report >= lo or report < hi)
        if covers:
            return minutes[seg_index]
    raise LookupError(f"no Table B band covers {report!r}")

Verify: resolve_table_b(time(5, 30), 4) returns 720 (12:00). A twelve-segment day clamps to the 7+ column, so resolve_table_b(time(5, 30), 12) returns 630 (10:30), identical to resolve_table_b(time(5, 30), 7) — the clamp collapses every leg count above seven onto the final column.

Step 3 — Assert every boundary

The cells that break in production are the band edges, so the assertions pin both sides of each boundary rather than sampling the interior.

# Band boundaries are inclusive-low, exclusive-high.
assert resolve_table_b(time(5, 0), 1) == 720    # first instant of 0500-0559
assert resolve_table_b(time(4, 59), 1) == 600   # last instant of 0400-0459
assert resolve_table_b(time(7, 0), 1) == 840    # daytime peak opens at 0700
assert resolve_table_b(time(11, 59), 7) == 690  # still in the 0700-1159 band
assert resolve_table_b(time(23, 30), 3) == 540  # midnight-wrapping band
assert resolve_table_b(time(0, 0), 1) == 540    # 0000 opens the first band

Each assertion fixes a boundary a naive <=/< swap would move by one minute — the failure mode that returns a neighbouring band’s ceiling and, once a quarter, an illegal duty day.

Table B FDP ceiling across report time by segment count A step chart of the Table B maximum flight duty period against acclimatised report time for one segment, four segments, and seven-plus segments. All three curves peak in the 0700-1159 daytime band and drop for early-morning and late-night reports that encroach on the window of circadian low; more segments lower the ceiling at every report time. 14h 11h 9h 00 08 16 24 1 segment 4 seg 7+
Table B as a shape: the ceiling peaks for daytime reports and collapses toward the 9-hour floor for early-morning and late-night starts, with each added segment pulling every point down.

Failure modes and troubleshooting

Frequently Asked Questions

Does Table B apply to augmented operations?

No. Table B is the unaugmented grid under §117.13. Augmented operations with a relief crew use the separate Table C under §117.17, which is seeded as its own effective-dated grid and resolved by rest-facility class, not by this resolver.

Why is the ceiling highest for a 0700–1159 report?

Because a mid-morning report avoids encroaching on the window of circadian low, so the regulation permits the longest duty day — up to 14 hours for one or two segments. Reports before 05:00 or after 22:00 clip the WOCL and are held near the 9-hour floor.

What does the 7+ column mean for a ten-segment day?

The same limit as a seven-segment day. Table B stops distinguishing segment counts at seven, so every day with seven or more segments reads the final column. The resolver enforces this by clamping.

How do I roll the grid forward when the FAA amends it?

Insert a new effective-dated ruleset with the amended cells and set the previous ruleset’s effective_to. The resolver selects by operation date, so historical verdicts continue to resolve against the grid that was in force when the schedule was published.

Back to Regulatory Reference Tables.