Skip to content

Instantly share code, notes, and snippets.

@sashka
Last active June 22, 2016 15:01
Show Gist options
  • Select an option

  • Save sashka/2cecdf0a68a9c986ab99619feb7368ee to your computer and use it in GitHub Desktop.

Select an option

Save sashka/2cecdf0a68a9c986ab99619feb7368ee to your computer and use it in GitHub Desktop.
# encoding: utf-8
# (c) 2016 Alexander Saltanov <[email protected]>
"""
Aircraft tail number parsing and formatting library for Python
Examples of use:
>>>
>>> n = parse('v-q b-l-n')
>>> print n
Formatted Number: VQ-BLN, Normalized Number: vqbln, Possible Number: True, Valid Number: True, Country: Bermuda (BMU)
>>> x = parse('vq bl9')
>>> print x
Formatted Number: None, Normalized Number: vqbl9, Possible Number: True, Valid Number: False, Country: None
>>> y = parse('G-KANO')
>>> print y
Formatted Number: G-KANO, Normalized Number: gkano, Possible Number: True, Valid Number: True, Country: United Kingdom (GBR)
>>> is_possible_number('vq123')
False
>>> is_valid_number('vqblk')
True
>>> is_valid_number('p4sa1')
False
"""
import re
__all__ = ['TailNumber', 'is_possible_number', 'is_valid_number', 'parse']
# Data source:
# https://en.wikipedia.org/wiki/List_of_aircraft_registration_prefixes
# http://unstats.un.org/unsd/tradekb/Knowledgebase/Country-Code
# TODO(asd): When raw prefixes are processed extra information should be included into trie (digits in ranges, lengths, etc.)
RAW_PREFIXES = (
# Prefix, Valid range begin, Valid range end, Country code, Extra params dict
('YA', 'YA-AAA', 'YA-ZZZ', 'AFG', {'country_name': 'Afghanistan'}),
('ZA', 'ZA-AAA', 'ZA-ZZZ', 'ALB', {'country_name': 'Albania'}),
('7T', '7T-AAA', '7T-ZZZ', 'DZA', {'country_name': 'Algeria'}),
('C3', 'C3-AAA', 'C3-ZZZ', 'AND', {'country_name': 'Andorra'}),
('G', 'G-AAAA', 'G-ZZZZ', 'GBR', {'country_name': 'United Kingdom'}),
('M', 'M-AAAA', 'M-ZZZZ', 'IMN', {'country_name': 'Isle of Man'}),
('OE', 'OE-AAA', 'OE-KZZ', 'AUT', {'country_name': 'Austria'}),
('OE', 'OE-BAA', 'OE-BZZ', 'AUT', {'country_name': 'Austria', 'official_use': True}),
('OE', 'OE-LAA', 'OE-LZZ', 'AUT', {'country_name': 'Austria', 'airline': True, 'sheduled_flights': True}),
('OE', 'OE-VAA', 'OE-VZZ', 'AUT', {'country_name': 'Austria', 'test': True}),
('OE', 'OE-WAA', 'OE-WZZ', 'AUT', {'country_name': 'Austria', 'aircraft_type': 'amphibian'}),
('OE', 'OE-XAA', 'OE-XZZ', 'AUT', {'country_name': 'Austria', 'aircraft_type': 'helicopter'}),
('OE', 'OE-0001', 'OE-5999', 'AUT', {'country_name': 'Austria', 'aircraft_type': 'glider'}),
('OE', 'OE-9000', 'OE-9999', 'AUT', {'country_name': 'Austria', 'aircraft_type': 'motor glider'}),
('P4', 'P4-AAA', 'P4-ZZZ', 'ABW', {'country_name': 'Aruba'}),
('T7', 'T7-001', 'T7-999', 'SMR', {'country_name': 'San Marino', 'aircraft_type': 'microlight'}),
('T7', 'T7-AAA', 'T7-ZZZ', 'SMR', {'country_name': 'San Marino'}),
('9H', '9H-AAA', '9H-ZZZ', 'MLT', {'country_name': 'Malta'}),
('LX', 'LX-AAA', 'LX-ZZZ', 'LUX', {'country_name': 'Luxembourg'}),
('LX', 'LX-BAA', 'LX-BZZ', 'LUX', {'country_name': 'Luxembourg', 'aircraft_type': 'balloon'}),
('LX', 'LX-CAA', 'LX-CZZ', 'LUX', {'country_name': 'Luxembourg', 'aircraft_type': 'glider or motor glider'}),
('LX', 'LX-HAA', 'LX-HZZ', 'LUX', {'country_name': 'Luxembourg', 'aircraft_type': 'helicopter'}),
('LX', 'LX-N90442', 'LX-N90459', 'LUX', {'country_name': 'Luxembourg', 'aircraft_type': 'NATO AWACS'}),
('LX', 'LX-XAA', 'LX-XZZ', 'LUX', {'country_name': 'Luxembourg', 'aircraft_type': 'ultralight'}),
('OY', 'OY-AAA', 'OY-ZZZ', 'DNK', {'country_name': 'Denmark'}),
('OY', 'OY-HAA', 'OY-HZZ', 'DNK', {'country_name': 'Denmark', 'aircraft_type': 'helicopter'}),
('OY', 'OY-BAA', 'OY-BZZ', 'DNK', {'country_name': 'Denmark', 'aircraft_type': 'balloon'}),
('VP-C', 'VP-CAA', 'VP-CZZ', 'CYM', {'country_name': 'Cayman Islands'}),
('VP-B', 'VP-BAA', 'VP-BZZ', 'BMU', {'country_name': 'Bermuda'}),
('VQ-B', 'VQ-BAA', 'VQ-BZZ', 'BMU', {'country_name': 'Bermuda'}),
)
class TailNumber(object):
def __init__(self, number=None):
self.raw = number
self.normalized = None
self.formatted = None
self.country_code = None
self.country_name = None
self.params = None
self.is_commercial = None
self.is_military = None
self.is_possible = False
self.is_valid = False
self.rule_matched = None
self._debug_fields = ['raw', 'normalized', 'formatted', 'params', 'rule_matched', 'is_possible', 'is_valid']
self.parse()
def __unicode__(self):
if self.country_name is not None:
country = '%s (%s)' % (self.country_name, self.country_code)
else:
country = 'None'
return u'Formatted Number: %s, Normalized Number: %s, Possible Number: %s, Valid Number: %s, Country: %s' % (self.formatted, self.normalized, self.is_possible, self.is_valid, country)
def __str__(self):
return unicode(self).encode('utf-8')
def __repr__(self):
tmp = ['<TailNumber']
for k in self._debug_fields:
tmp.append(u' %s=%s' % (k, getattr(self, k, '')))
tmp.append('>')
return ''.join(tmp)
def _normalize(self, v):
return v.replace(' ', '').replace('-', '').lower()
def _format(self):
r = self.rule_matched
prefix = self._normalize(r[0])
rest = self.normalized.split(prefix)[1]
tmp = []
tmp.append(r[0])
if '-' not in r[0]:
tmp.append('-')
tmp.append(rest.upper())
return ''.join(tmp)
def _less_or_equal(self, a, b):
# Quick and dirty tail number comparison returns: `True` if a <= b, `False` otherwise.
# To be rewritten!
digits_re = re.compile('\d')
def __meta(v):
return len(v), bool(digits_re.search(v))
assert(isinstance(a, (str, unicode)))
assert(isinstance(b, (str, unicode)))
a_len, a_has_digits = __meta(a)
b_len, b_has_digits = __meta(b)
if a_len == b_len and a_has_digits == b_has_digits and a <= b:
return True
return False
def parse(self):
self.normalized = self._normalize(self.raw)
for r in RAW_PREFIXES:
prefix, range_begin, range_end, country_code, params = r
prefix = self._normalize(prefix)
range_begin_normalized = self._normalize(range_begin)
range_end_normalized = self._normalize(range_end)
if self.normalized.startswith(prefix):
self.is_possible = True
no_prefix_number = self.normalized.split(prefix)[1]
no_prefix_range_begin = range_begin_normalized.split(prefix)[1]
no_prefix_range_end = range_end_normalized.split(prefix)[1]
if self._less_or_equal(no_prefix_range_begin, no_prefix_number) and self._less_or_equal(no_prefix_number, no_prefix_range_end):
self.is_valid = True
self.rule_matched = r
self.country_code = country_code
self.params = params
self.country_name = params['country_name']
self.formatted = self._format()
break
def parse(number):
return TailNumber(number)
def is_possible_number(numobj):
if not isinstance(numobj, TailNumber):
numobj = TailNumber(numobj)
return numobj.is_possible
def is_valid_number(numobj):
if not isinstance(numobj, TailNumber):
numobj = TailNumber(numobj)
return numobj.is_valid
if __name__ == '__main__':
import doctest
doctest.testmod()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment