Last active
June 22, 2016 15:01
-
-
Save sashka/2cecdf0a68a9c986ab99619feb7368ee to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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