UNPKG

snailmailaddressparser

Version:

A Javascript library to parse snail mail addresses into components

127 lines (107 loc) 3.74 kB
# # LineMatcher # ----------- # # A LineMatcher test corresponds to a single line of a bigger address parsing # strategy. An address parsing strategy will usually be made up of more than # one line strategies in an array. If every LineMatcher matches every line of # an address, then the address matches. If not, the LineMatcher name can # give us some idea of where the match went wrong. # # Line matcher has built-in unit tests, by way of valid_tests and invalid_tests # so that we can localize the regular expression and some examples that it will # be tested against. # class LineMatcher constructor: (@name, @expression, options={}) -> @options = _.defaults(options, invalid_tests: [] is_optional: false rex_flags: 'xi' valid_tests: [] _or: null ) @rex = XRegExp("^#{expression}$", @options.rex_flags) # return a list of names this could match names: () -> if @options._or return "#{@name} or #{@options._or.names()}" else return @name # if the default is !is_optional, or unknown, one can use the optional() call # to get a copy of a LineMatcher that is optional (or mandatory, below) optional: () -> copy = @clone() copy.options.is_optional = true return copy mandatory: () -> copy = @clone() copy.options.is_optional = false return copy # # An argument is optional if it is, or any of its alternates are, optional # is_optional: () -> if @options.is_optional return true if @options._or return @options._or.is_optional() return false # # Return a copy of this LineMatcher # clone: () -> new LineMatcher(@name, @expression, @options) # # `or` adds alternative matchers to this one, so we can say e.g. # STREET.or(STREET_UNIT).or(UNIT_STREET) and any may match. # # Returns a new LineMatcher instance # or: (matcher) -> lm = @ if _.isObject @options._or # we already have an alt; push this down if @options._or.name == matcher.name # we have this matcher already as an alt return @ @options._or = @options._or.or(matcher) # add a new leaf else # return a copy --- we don't want to permanently modify this # LineMatcher # i.e. STREET.or(UNIT_STREET) # should not mean that subsequent uses of STREET should also match # UNIT_STREET lm = @clone() lm.options._or = matcher # return this, so these can be chained ie # X.or(Y).or(Z) makes X.or = Y and Y.or = Z return lm # match # ~~~~~ # Return an object mapping matched items if the line matches, null otherwise # match: (line, check_or=true) -> matches = XRegExp.exec(line, @rex) if matches == null if check_or # <-- used by unit tests to isolate matchers return @options._or?.match?(line) or null else return check_or # console.log("Match of \"#{line}\" against #{@name}: \"#{@expression}\"") # Filter out numeric indexes and XRegExp hard-coded 'index' and 'input' # properties EXCLUDED = ['index', 'input'] # ignore these properties matched_properties = {} # matches.keys is [0,1,2,input,index, <our properties>]; the # 0,1,2,index,input are all added by XRegExp. We want the named properties # returned. _.each(_.keys(matches), (key) -> if key not in EXCLUDED and isNaN(key) matched_properties[key] = matches[key] ) # matches is now e.g. { addressee: 'John' } return matched_properties # the following tells the tester that this is a linematcher class # the alternative is eg # lminstance.__proto__.constructor.name == "LineMatcher" # which prevents subclassing isLineMatcherClass: true