efj-parser 0.9.2__tar.gz → 0.9.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {efj_parser-0.9.2 → efj_parser-0.9.4}/PKG-INFO +11 -2
- efj_parser-0.9.4/efj_parser/__init__.py +442 -0
- {efj_parser-0.9.2 → efj_parser-0.9.4}/efj_parser.egg-info/PKG-INFO +11 -2
- {efj_parser-0.9.2 → efj_parser-0.9.4}/setup.py +1 -1
- efj_parser-0.9.4/tests/test_parser.py +758 -0
- efj_parser-0.9.2/efj_parser/__init__.py +0 -383
- efj_parser-0.9.2/tests/test_parser.py +0 -467
- {efj_parser-0.9.2 → efj_parser-0.9.4}/LICENSE +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.4}/README.md +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.4}/efj_parser/py.typed +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.4}/efj_parser.egg-info/SOURCES.txt +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.4}/efj_parser.egg-info/dependency_links.txt +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.4}/efj_parser.egg-info/top_level.txt +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.4}/pyproject.toml +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: efj-parser
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: Parse an electronic Flight Journal file
|
|
5
5
|
Home-page: https://github.com/JonHurst/efj_parser
|
|
6
6
|
Author: Jon Hurst
|
|
@@ -13,6 +13,15 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.11
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: project-url
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
16
25
|
|
|
17
26
|
# eFJ Parser #
|
|
18
27
|
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
from typing import NamedTuple, Optional, Callable, Union, cast
|
|
4
|
+
import datetime as dt
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Duty(NamedTuple):
|
|
8
|
+
start: dt.datetime #: Start of duty
|
|
9
|
+
duration: int #: Length of duty in minutes
|
|
10
|
+
ftl_correction: int = 0 #: Minutes to reduce duty length for FTL
|
|
11
|
+
extra_flags: tuple[str, ...] = () #: Any unprocessed flags
|
|
12
|
+
comment: str = "" #: Remarks
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Crewmember(NamedTuple):
|
|
16
|
+
role: str #: The crewmember's role (e.g. ``CP``, ``FO``, ``PU``, ``FA``)
|
|
17
|
+
name: str #: The crewmember's name
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Roles(NamedTuple):
|
|
21
|
+
p1: int = 0 #: Minutes operating as p1
|
|
22
|
+
p1s: int = 0 #: Minutes operating as p1s
|
|
23
|
+
p2: int = 0 #: Minutes operating as p2
|
|
24
|
+
put: int = 0 #: Minutes operating as put
|
|
25
|
+
instructor: int = 0 #: Minutes operating as instructor
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Conditions(NamedTuple):
|
|
29
|
+
night: int = 0 #: Minutes operating at night
|
|
30
|
+
ifr: int = 0 #: Minutes operating under IFR
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Landings(NamedTuple):
|
|
34
|
+
day: int = 0 #: Number of day landings
|
|
35
|
+
night: int = 0 #: Number of night landings
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Aircraft(NamedTuple):
|
|
39
|
+
reg: str #: The registration of the aircraft
|
|
40
|
+
type_: str #: The type of the aircraft
|
|
41
|
+
class_: str #: Class: ``spse``, ``spme`` or ``mc``
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Airports(NamedTuple):
|
|
45
|
+
origin: str #: The origin airport
|
|
46
|
+
dest: str #: The destination airport
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Sector(NamedTuple):
|
|
50
|
+
start: dt.datetime #: Off chocks date and time
|
|
51
|
+
total: int #: Minutes between off chocks and on chocks
|
|
52
|
+
roles: Roles #: Minutes operating in each role
|
|
53
|
+
conditions: Conditions #: Minutes operating in ifr and at night
|
|
54
|
+
landings: Landings #: Number of day and night landings
|
|
55
|
+
aircraft: Aircraft #: Type, registration and class of the aircraft
|
|
56
|
+
airports: Airports #: Origin and destination airports
|
|
57
|
+
captain: str #: Name(s) of captain(s), Self for oneself
|
|
58
|
+
extra_flags: tuple[str, ...] #: Any flags not processed by the parser
|
|
59
|
+
comment: str #: Remarks
|
|
60
|
+
crew: tuple[Crewmember, ...] #: A (possibly empty) list of crew
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ValidationError(Exception):
|
|
64
|
+
"""An problem was found in the input data."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, line_number, message, problem_string):
|
|
67
|
+
self.line_number = line_number
|
|
68
|
+
"""The 1-indexed line number where the problem was found."""
|
|
69
|
+
self.message = message
|
|
70
|
+
"""A message describing the problem."""
|
|
71
|
+
self.problem_string = problem_string
|
|
72
|
+
"""The string that caused the problem."""
|
|
73
|
+
|
|
74
|
+
def __str__(self):
|
|
75
|
+
return (f"Line {self.line_number}: "
|
|
76
|
+
f"[{self.message}] {self.problem_string}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class _VE(Exception):
|
|
80
|
+
|
|
81
|
+
class Code(IntEnum):
|
|
82
|
+
BAD_CREWLIST = 100
|
|
83
|
+
BAD_DATE = 110
|
|
84
|
+
BAD_FLAGS = 120
|
|
85
|
+
BAD_ROLE_FLAGS = 121
|
|
86
|
+
BAD_CONDITION_FLAGS = 122
|
|
87
|
+
BAD_SYNTAX = 130
|
|
88
|
+
BAD_TIME = 140
|
|
89
|
+
MISSING_AIRCRAFT = 200
|
|
90
|
+
MISSING_CAPTAIN = 210
|
|
91
|
+
MISSING_DATE = 220
|
|
92
|
+
MISSING_DEST = 230
|
|
93
|
+
MISSING_ORIGIN = 240
|
|
94
|
+
|
|
95
|
+
message_lookup = {
|
|
96
|
+
Code.BAD_CREWLIST: "Invalid crew list",
|
|
97
|
+
Code.BAD_DATE: "Invalid date format",
|
|
98
|
+
Code.BAD_FLAGS: "Invalid flags",
|
|
99
|
+
Code.BAD_ROLE_FLAGS: "Invalid role flags",
|
|
100
|
+
Code.BAD_CONDITION_FLAGS: "Invalid flight condition flags",
|
|
101
|
+
Code.BAD_SYNTAX: "Invalid syntax",
|
|
102
|
+
Code.BAD_TIME: "Invalid time format",
|
|
103
|
+
Code.MISSING_AIRCRAFT: "Prior aircraft specifier required",
|
|
104
|
+
Code.MISSING_AIRCRAFT: "No Captain specified",
|
|
105
|
+
Code.MISSING_DATE: "Prior date specifier required",
|
|
106
|
+
Code.MISSING_DEST: "No destination airfield specified",
|
|
107
|
+
Code.MISSING_ORIGIN: "No origin airfield specified",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def __init__(self, code: Code):
|
|
111
|
+
self.code = code
|
|
112
|
+
self.message = _VE.message_lookup.get(code, "Unknown error")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
ParseRet = Union[dt.date, Duty, Aircraft, list[Crewmember], Sector, str]
|
|
116
|
+
ParseHook = Optional[Callable[[str, int, str, ParseRet], None]]
|
|
117
|
+
Flag = tuple[str, Optional[int]]
|
|
118
|
+
Flags = tuple[Flag, ...]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Parser():
|
|
122
|
+
"""Parser for electronic Flight Journal (eFJ) files."""
|
|
123
|
+
|
|
124
|
+
__RE_SECTOR = re.compile(
|
|
125
|
+
r"""(\w*)/(\w*) # origin/dest (both optional)
|
|
126
|
+
\s+(\d{4})/(\d{4}) # times
|
|
127
|
+
(?>\s+([^#]+))? # flags
|
|
128
|
+
(?>\s*\#(.*))? # comment
|
|
129
|
+
""", re.VERBOSE)
|
|
130
|
+
__RE_CREWLIST = re.compile(r"\{([^}]*)}")
|
|
131
|
+
__RE_DATE = re.compile(r"(\d{4}-\d{2}-\d{2})")
|
|
132
|
+
__RE_AIRCRAFT = re.compile(
|
|
133
|
+
r"""(?>([-\w]+) # registration
|
|
134
|
+
\s*:\s*
|
|
135
|
+
([-\w]+)) # type
|
|
136
|
+
(?:\s*:\s*
|
|
137
|
+
(mc|spse|spme))? # optional class""", re.VERBOSE)
|
|
138
|
+
__RE_DUTY = re.compile(
|
|
139
|
+
r"""(\d{4})/(\d{4}) # times
|
|
140
|
+
(?>\s+([^#]+))? # flags
|
|
141
|
+
(?>\s*\#(.*))? # comment
|
|
142
|
+
""", re.VERBOSE)
|
|
143
|
+
__RE_SHORTDATE = re.compile(r"(\++)")
|
|
144
|
+
__RE_COMMENT = re.compile(r"#(.*)")
|
|
145
|
+
|
|
146
|
+
def __init__(self) -> None:
|
|
147
|
+
self.date: Optional[dt.date] = None
|
|
148
|
+
self.airports = Airports("", "")
|
|
149
|
+
self.aircraft = Aircraft("", "", "")
|
|
150
|
+
self.class_lookup: dict[str, str] = {}
|
|
151
|
+
self.crewlist: list[Crewmember] = []
|
|
152
|
+
|
|
153
|
+
def __parse_date(self, mo: re.Match) -> dt.date:
|
|
154
|
+
assert mo
|
|
155
|
+
try:
|
|
156
|
+
self.date = dt.date.fromisoformat(mo.group(1))
|
|
157
|
+
return self.date
|
|
158
|
+
except ValueError:
|
|
159
|
+
raise _VE(_VE.Code.BAD_DATE)
|
|
160
|
+
|
|
161
|
+
def __parse_nextdate(self, mo: re.Match) -> dt.date:
|
|
162
|
+
assert mo
|
|
163
|
+
if not self.date:
|
|
164
|
+
raise _VE(_VE.Code.MISSING_DATE)
|
|
165
|
+
self.date += dt.timedelta(len(mo.group(1)))
|
|
166
|
+
return self.date
|
|
167
|
+
|
|
168
|
+
def __parse_duty(self, mo: re.Match) -> Duty:
|
|
169
|
+
assert mo
|
|
170
|
+
if not self.date:
|
|
171
|
+
raise _VE(_VE.Code.MISSING_DATE)
|
|
172
|
+
start_str, end_str, flags, comment = (
|
|
173
|
+
X.strip() if X else "" for X in mo.group(1, 2, 3, 4))
|
|
174
|
+
dt_start, duration = self.__parse_times(start_str, end_str)
|
|
175
|
+
ftl_correction = 0
|
|
176
|
+
unused_flags = []
|
|
177
|
+
for f in _split_flags(flags):
|
|
178
|
+
if f[0] == "r":
|
|
179
|
+
ftl_correction += f[1] if f[1] else duration
|
|
180
|
+
else:
|
|
181
|
+
unused_flags.append(f)
|
|
182
|
+
return Duty(dt_start, duration, ftl_correction,
|
|
183
|
+
_join_flags(tuple(unused_flags)), comment)
|
|
184
|
+
|
|
185
|
+
def __parse_aircraft(self, mo: re.Match) -> Aircraft:
|
|
186
|
+
assert mo
|
|
187
|
+
reg, type_, class_ = (X.strip() if X else ""
|
|
188
|
+
for X in mo.group(1, 2, 3))
|
|
189
|
+
if class_:
|
|
190
|
+
self.class_lookup[type_] = class_
|
|
191
|
+
else:
|
|
192
|
+
class_ = self.class_lookup.get(type_, "")
|
|
193
|
+
self.aircraft = Aircraft(reg, type_, class_)
|
|
194
|
+
return self.aircraft
|
|
195
|
+
|
|
196
|
+
def __parse_crewlist(self, mo: re.Match) -> list[Crewmember]:
|
|
197
|
+
self.crewlist = []
|
|
198
|
+
try:
|
|
199
|
+
crew = mo.group(1).strip()
|
|
200
|
+
if not crew:
|
|
201
|
+
return []
|
|
202
|
+
for role, name in [X.strip().split(":", 1)
|
|
203
|
+
for X in crew.split(",")]:
|
|
204
|
+
role = role.strip()
|
|
205
|
+
if not role or " " in role:
|
|
206
|
+
raise ValueError
|
|
207
|
+
name = " ".join([X.strip() for X in name.split()])
|
|
208
|
+
if not name or ":" in name:
|
|
209
|
+
raise ValueError
|
|
210
|
+
self.crewlist.append(Crewmember(role, name))
|
|
211
|
+
except ValueError:
|
|
212
|
+
raise _VE(_VE.Code.BAD_CREWLIST)
|
|
213
|
+
return self.crewlist
|
|
214
|
+
|
|
215
|
+
def __parse_times(
|
|
216
|
+
self, t_start: str, t_end: str
|
|
217
|
+
) -> tuple[dt.datetime, int]:
|
|
218
|
+
assert self.date
|
|
219
|
+
assert t_start and t_end
|
|
220
|
+
assert t_start.isnumeric() and t_end.isnumeric()
|
|
221
|
+
try:
|
|
222
|
+
ts = dt.time.fromisoformat(t_start) # Off blocks
|
|
223
|
+
te = dt.time.fromisoformat(t_end) # On blocks
|
|
224
|
+
except (ValueError, TypeError):
|
|
225
|
+
raise _VE(_VE.Code.BAD_TIME)
|
|
226
|
+
duration = (te.hour - ts.hour) * 60 + (te.minute - ts.minute)
|
|
227
|
+
if duration < 0:
|
|
228
|
+
duration += 1440
|
|
229
|
+
return (dt.datetime.combine(self.date, ts), duration)
|
|
230
|
+
|
|
231
|
+
def __parse_sector_flags(
|
|
232
|
+
self,
|
|
233
|
+
flags: Flags,
|
|
234
|
+
duration: int
|
|
235
|
+
) -> tuple[Conditions, Roles, Landings, Flags]:
|
|
236
|
+
conditions, unused_flags = _process_conditions(flags, duration)
|
|
237
|
+
roles, unused_flags = _process_roles(unused_flags, duration)
|
|
238
|
+
if roles.p2 == duration:
|
|
239
|
+
landings = Landings()
|
|
240
|
+
else:
|
|
241
|
+
landings, unused_flags = _process_landings(unused_flags, duration,
|
|
242
|
+
conditions.night)
|
|
243
|
+
return conditions, roles, landings, unused_flags
|
|
244
|
+
|
|
245
|
+
def __captain(self, roles, duration) -> str:
|
|
246
|
+
captains = ["Self"] if roles.p1 else []
|
|
247
|
+
if roles.p1 < duration:
|
|
248
|
+
for m in self.crewlist:
|
|
249
|
+
if m.role.upper() == "CP":
|
|
250
|
+
captains.append(m.name)
|
|
251
|
+
return ", ".join(captains)
|
|
252
|
+
|
|
253
|
+
def __parse_sector(self, mo: re.Match) -> Sector:
|
|
254
|
+
if not self.date:
|
|
255
|
+
raise _VE(_VE.Code.MISSING_DATE)
|
|
256
|
+
if not (self.aircraft.reg and self.aircraft.type_):
|
|
257
|
+
raise _VE(_VE.Code.MISSING_AIRCRAFT)
|
|
258
|
+
(origin, dest, start_str, end_str, flag_str, comment
|
|
259
|
+
) = (X.strip() if X else "" for X in mo.group(*range(1, 7)))
|
|
260
|
+
self.airports = Airports(
|
|
261
|
+
origin or self.airports.dest,
|
|
262
|
+
dest or self.airports.origin
|
|
263
|
+
)
|
|
264
|
+
if not self.airports.origin:
|
|
265
|
+
raise _VE(_VE.Code.MISSING_ORIGIN)
|
|
266
|
+
if not self.airports.dest:
|
|
267
|
+
raise _VE(_VE.Code.MISSING_DEST)
|
|
268
|
+
start, duration = self.__parse_times(start_str, end_str)
|
|
269
|
+
try:
|
|
270
|
+
flags = _split_flags(flag_str)
|
|
271
|
+
except ValueError:
|
|
272
|
+
raise _VE(_VE.Code.BAD_FLAGS)
|
|
273
|
+
conditions, roles, landings, unused_flags = (
|
|
274
|
+
self.__parse_sector_flags(flags, duration))
|
|
275
|
+
captain = self.__captain(roles, duration)
|
|
276
|
+
if not captain:
|
|
277
|
+
raise _VE(_VE.Code.MISSING_CAPTAIN)
|
|
278
|
+
return Sector(
|
|
279
|
+
start, duration, roles, conditions,
|
|
280
|
+
landings, self.aircraft, self.airports,
|
|
281
|
+
captain, _join_flags(unused_flags),
|
|
282
|
+
comment,
|
|
283
|
+
tuple(self.crewlist))
|
|
284
|
+
|
|
285
|
+
def __parse_comment(self, mo: re.Match) -> str:
|
|
286
|
+
return mo.group(1) or ""
|
|
287
|
+
|
|
288
|
+
def parse(
|
|
289
|
+
self, s: str,
|
|
290
|
+
hook: ParseHook = None
|
|
291
|
+
) -> tuple[tuple[Duty, ...], tuple[Sector, ...]]:
|
|
292
|
+
"""Extract duties and sectors from an eFJ string
|
|
293
|
+
|
|
294
|
+
:param s: A string containing data in eFJ format
|
|
295
|
+
:param hook: A function that, if specified, will be called after each
|
|
296
|
+
line is processed. The function will be called with the positional
|
|
297
|
+
parameters line text, line number, line type and parsed object.
|
|
298
|
+
Line type may be "blank", "comment", "date", "short_date",
|
|
299
|
+
"aircraft", "crewlist" or "sector".
|
|
300
|
+
:return: Two tuples containing the extracted data. The first is a tuple
|
|
301
|
+
of Duty structures and the second is a tuple of Sector structures.
|
|
302
|
+
|
|
303
|
+
"""
|
|
304
|
+
duties: list[Duty] = []
|
|
305
|
+
sectors: list[Sector] = []
|
|
306
|
+
func_map = [
|
|
307
|
+
(Parser.__RE_SECTOR, self.__parse_sector, "sector"),
|
|
308
|
+
(Parser.__RE_CREWLIST, self.__parse_crewlist, "crewlist"),
|
|
309
|
+
(Parser.__RE_DATE, self.__parse_date, "date"),
|
|
310
|
+
(Parser.__RE_DUTY, self.__parse_duty, "duty"),
|
|
311
|
+
(Parser.__RE_AIRCRAFT, self.__parse_aircraft, "aircraft"),
|
|
312
|
+
(Parser.__RE_SHORTDATE, self.__parse_nextdate, "short_date"),
|
|
313
|
+
(Parser.__RE_COMMENT, self.__parse_comment, "comment"),
|
|
314
|
+
]
|
|
315
|
+
for c, line in enumerate(s.splitlines()):
|
|
316
|
+
line = line.strip()
|
|
317
|
+
if not line:
|
|
318
|
+
hook and hook(line, c, 'blank', "")
|
|
319
|
+
continue
|
|
320
|
+
ret = None
|
|
321
|
+
for rexp, func, entry_type in func_map:
|
|
322
|
+
if mo := rexp.fullmatch(line):
|
|
323
|
+
try:
|
|
324
|
+
ret = func(mo)
|
|
325
|
+
except _VE as e:
|
|
326
|
+
raise ValidationError(c + 1, e.message, mo.group(0))
|
|
327
|
+
hook and hook(line, c + 1, entry_type, cast(ParseRet, ret))
|
|
328
|
+
break
|
|
329
|
+
else:
|
|
330
|
+
raise ValidationError(
|
|
331
|
+
c + 1, _VE(_VE.Code.BAD_SYNTAX).message, line)
|
|
332
|
+
if isinstance(ret, Duty):
|
|
333
|
+
duties.append(ret)
|
|
334
|
+
elif isinstance(ret, Sector):
|
|
335
|
+
sectors.append(ret)
|
|
336
|
+
return (tuple(duties), tuple(sectors))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _split_flags(flag_str: str) -> Flags:
|
|
340
|
+
"""Clean and split flag string.
|
|
341
|
+
|
|
342
|
+
:param flag_str: Input string such as "p2:20 ln:1 ins"
|
|
343
|
+
:returns: tuple of pairs like (("p2", 20), ("ln", 1), ("ins", None), ...)
|
|
344
|
+
:raises: ValueError if RHS of : is not convertible to integer
|
|
345
|
+
"""
|
|
346
|
+
fields = flag_str.split()
|
|
347
|
+
if not fields:
|
|
348
|
+
return ()
|
|
349
|
+
return tuple((L[0], int(L[1])) if len(L) == 2 else (L[0], None)
|
|
350
|
+
for L in (X.split(":", 1) for X in fields))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _join_flags(flags: Flags) -> tuple[str, ...]:
|
|
354
|
+
"""Join Flags into a tuple of str"""
|
|
355
|
+
if not flags:
|
|
356
|
+
return ()
|
|
357
|
+
return tuple(f"{X[0]}:{X[1]}" if X[1] else X[0] for X in flags)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _process_landings(
|
|
361
|
+
flags: Flags,
|
|
362
|
+
duration: int,
|
|
363
|
+
night: int
|
|
364
|
+
) -> tuple[Landings, Flags]:
|
|
365
|
+
night_ldg, day_ldg = 0, 0
|
|
366
|
+
found_landing_flag = False
|
|
367
|
+
unused: list[Flag] = []
|
|
368
|
+
for f in flags:
|
|
369
|
+
match f[0]:
|
|
370
|
+
case "m":
|
|
371
|
+
found_landing_flag = True
|
|
372
|
+
case "ld":
|
|
373
|
+
day_ldg += 1 if f[1] is None else f[1]
|
|
374
|
+
found_landing_flag = True
|
|
375
|
+
case "ln":
|
|
376
|
+
night_ldg += 1 if f[1] is None else f[1]
|
|
377
|
+
found_landing_flag = True
|
|
378
|
+
case _:
|
|
379
|
+
unused.append(f)
|
|
380
|
+
if not found_landing_flag:
|
|
381
|
+
if duration == night:
|
|
382
|
+
night_ldg = 1
|
|
383
|
+
else:
|
|
384
|
+
day_ldg = 1
|
|
385
|
+
return Landings(day_ldg, night_ldg), tuple(unused)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _process_roles(flags: Flags, duration: int) -> tuple[Roles, Flags]:
|
|
389
|
+
"""Extract role durations from sector flags
|
|
390
|
+
|
|
391
|
+
:param flags: Sector flags
|
|
392
|
+
:param duration: Total duration of sector
|
|
393
|
+
:return: A Roles object and the flags input parameter with processed flags
|
|
394
|
+
removed.
|
|
395
|
+
"""
|
|
396
|
+
p1s, p2, put, p0, ins = 0, 0, 0, 0, 0
|
|
397
|
+
unused: list[Flag] = []
|
|
398
|
+
for f in flags:
|
|
399
|
+
match f[0]:
|
|
400
|
+
case "p1s":
|
|
401
|
+
p1s += f[1] if f[1] else duration
|
|
402
|
+
case "p2":
|
|
403
|
+
p2 += f[1] if f[1] else duration
|
|
404
|
+
case "put":
|
|
405
|
+
put += f[1] if f[1] else duration
|
|
406
|
+
case "p0":
|
|
407
|
+
p0 += f[1] if f[1] else duration
|
|
408
|
+
case "ins":
|
|
409
|
+
ins += f[1] if f[1] else duration
|
|
410
|
+
if ins > duration:
|
|
411
|
+
raise _VE(_VE.Code.BAD_ROLE_FLAGS)
|
|
412
|
+
case _:
|
|
413
|
+
unused.append(f)
|
|
414
|
+
p1 = duration - (p1s + p2 + put + p0)
|
|
415
|
+
if p1 < 0:
|
|
416
|
+
raise _VE(_VE.Code.BAD_ROLE_FLAGS)
|
|
417
|
+
return Roles(p1, p1s, p2, put, ins), tuple(unused)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _process_conditions(flags: Flags, dur: int) -> tuple[Conditions, Flags]:
|
|
421
|
+
"""Extract sector conditions from sector flags
|
|
422
|
+
|
|
423
|
+
:param flags: The flags as a tuple of strings
|
|
424
|
+
:param dur: The total duration of the sector
|
|
425
|
+
:return: A tuple of the form (Conditions, unused_flags) where unused flags
|
|
426
|
+
is the input tuple with used flags removed.
|
|
427
|
+
"""
|
|
428
|
+
night, vfr = 0, 0
|
|
429
|
+
unused: list[Flag] = []
|
|
430
|
+
for f in flags:
|
|
431
|
+
match f[0]:
|
|
432
|
+
case "n":
|
|
433
|
+
night += f[1] if f[1] else dur
|
|
434
|
+
if night > dur:
|
|
435
|
+
raise _VE(_VE.Code.BAD_CONDITION_FLAGS)
|
|
436
|
+
case "v":
|
|
437
|
+
vfr += f[1] if f[1] else dur
|
|
438
|
+
if vfr > dur:
|
|
439
|
+
raise _VE(_VE.Code.BAD_CONDITION_FLAGS)
|
|
440
|
+
case _:
|
|
441
|
+
unused.append(f)
|
|
442
|
+
return Conditions(night, dur - vfr), tuple(unused)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: efj-parser
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: Parse an electronic Flight Journal file
|
|
5
5
|
Home-page: https://github.com/JonHurst/efj_parser
|
|
6
6
|
Author: Jon Hurst
|
|
@@ -13,6 +13,15 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.11
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: project-url
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
16
25
|
|
|
17
26
|
# eFJ Parser #
|
|
18
27
|
|