/* * USPostalCodeService.java * Copyright (C) 2006 Amin Ahmad * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.ahmadsoft.postal; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * Provides a postal code service implementation for the United * States of America. * * @author Amin Ahmad */ public class USPostalCodeService { private PostalRetrievalStrategy retrievalStrategy; private static boolean firstUse = true; public USPostalCodeService() { if (firstUse) { firstUse = false; System.out.println( "U.S. Postal Code Service for Java version 1, Copyright (C) 2006 Amin Ahmad\n\n"+ "U.S. Postal Code Service for Java comes with ABSOLUTELY NO WARRANTY;\n"+ "This program is free software; you can redistribute it and/or modify\n"+ "it under the terms of the GNU General Public License as published by\n"+ "the Free Software Foundation; either version 2 of the License, or\n"+ "any later version." ); } } /** * Disposes an instance of this object. Subsequent use of this * object is a logical programming error. * @throws Exception */ public void dispose() throws Exception { } /** * Initializes an instance of the US Postal Code Service. Initialization * is required prior to use. * * @param retrievalStrategy an initialized postal retrieval strategy. * @throws Exception if an error occurs during initialization. Renders * this service instance unusable. */ public void initialize(PostalRetrievalStrategy retrievalStrategy) throws Exception { this.retrievalStrategy = retrievalStrategy; } /** * Returns a list of all recognized candidate cities for a given postal * code. Candidate cities are catogorized as actual, acceptable, and * unacceptable. * * @param postalCode the postal code. * @return a list of all recognized candidate cities for a given postal * code. */ public List getCandidates(int postalCode) { return retrievalStrategy.getCandidates(postalCode); } /** * Returns true if the given postal code is within * the given state, or false otherwise. For * example, isPostalCodeIn(85050, "AZ") will return true * because the 85050 postal code is within Arizona, but * isPostalCodeIn(43202, "CA") will return false * because the 43202 is not in California, but rather in Ohio. * * @param postalCode the postal code. * @param stateAbbr the two-digit, upper-case abbreviation for * a state, as specified in * United States Postal Service - Abbreviations. * @return true if the given postal code is within * the given state, or false otherwise. */ public boolean isPostalCodeIn(int postalCode, String stateAbbr) { List candidates = this.retrievalStrategy.getCandidates(postalCode); for (Iterator i=candidates.iterator();i.hasNext();) { PostalCodeEntry entry = (PostalCodeEntry) i.next(); if (entry.getState().equals(stateAbbr)) { return true; } } return false; } /** * Returns the PostalCodeEntry for the actual city * registered with the U.S. Post Office for this postal code. This * is a useful operation because every postal code has an official * city name associated with it, as well as several other names * recognized by the post office as acceptable or unacceptable. *

* For example, getActualFor(90064) will return a * PostalCodeEntry for Los Angeles, CA, which is * the official city for the 90064 postal code. Rancho Park, CA * is an acceptable, but not the actual, name. * * @param postalCode * @return the PostalCodeEntry for the actual city * registered with the U.S. Post Office for this postal code, or * null if there is no "actual" candidate for this postal code. * This may occur if the postal code is not yet assigned, or is * out of range. */ public PostalCodeEntry getActualFor(int postalCode) { List candidates = this.retrievalStrategy.getCandidates(postalCode); for (Iterator i=candidates.iterator();i.hasNext();) { PostalCodeEntry entry = (PostalCodeEntry) i.next(); if (entry.getEntryType() == PostalCodeConstants.CITY_ACTUAL) { return entry; } } return null; } /** * Performs a match using default matching options well-suited * to common validation. Specifically, ignore capitalization is * true, ignore punctuation is true, * ignore whitespace is false, and the minimum match * level is PostalCodeConstants.CITY_ACCEPTABLE. * * @param city the city to match. * @param postalCode the postal code within which to match the city. * @return the closest matches to the specified city within the specified * postal code. * @see #match(String, int, MatchOptions) */ public List match(String city, int postalCode) { return match(city, postalCode, new MatchOptions(true, true, false, PostalCodeConstants.CITY_ACCEPTABLE)); } /** * Returns a list of the closest matches to the specified city * within the specified postal code. The details of the matching * process can be controlled by specifying match options. *

* Note: This method does not take the state into consideration. Rather, * use #isPostalCodeIn(int, String) to determine if a postal code * is within a given state. * * @param city the city to match. * @param postalCode the postal code within which to match the city. * @param options parameters to control the matching process. * @return a list of the closest matches to the specified city within * the specfied postal code. * @see MatchOptions */ public List match(String city, int postalCode, MatchOptions options) { List results = new ArrayList(); List candidates = getCandidates(postalCode); StringBuffer sbCity = new StringBuffer(city.length()); for (int j=0; j -1) continue; if (options.isIgnoreCapitalization()) c = Character.toUpperCase(c); sbCity.append(c); } city = sbCity.toString(); // Two passes. The first pass determines the minimum value // while the second pass copies all values having that value // into the results array. // int min = Integer.MAX_VALUE; for (Iterator i=candidates.iterator();i.hasNext();) { PostalCodeEntry entry = (PostalCodeEntry) i.next(); if (entry.getEntryType() > options.getMaxMatchLevel()) continue; min = Math.min(min, StringUtils.getLevenshteinDistance(entry.getCity(), city)); } for (Iterator i=candidates.iterator();i.hasNext();) { PostalCodeEntry entry = (PostalCodeEntry) i.next(); if (entry.getEntryType() > options.getMaxMatchLevel()) continue; if (min == StringUtils.getLevenshteinDistance(entry.getCity(), city)) { results.add(new MatchResult(entry, min)); } } return results; } private static final String punctuation = "`~!@#$%^&*()_-+=[{]}\\|;:'\",<.>/?"; /** * Specifies options for performing an advanced match * operation. * * @author Amin Ahmad */ public static class MatchOptions { private boolean ignoreWhitespace=false; private boolean ignorePunctuation=true; private boolean ignoreCapitalization=true; private int maxMatchLevel=PostalCodeConstants.CITY_ACCEPTABLE; /** * Creates a new match option. * @param ignoreCapitalization * @param ignorePunctuation * @param ignoreWhitespace * @param maxMatchLevel */ public MatchOptions(boolean ignoreCapitalization, boolean ignorePunctuation, boolean ignoreWhitespace, int maxMatchLevel) { super(); this.ignoreCapitalization = ignoreCapitalization; this.ignorePunctuation = ignorePunctuation; this.ignoreWhitespace = ignoreWhitespace; this.maxMatchLevel = maxMatchLevel; } /** * Returns true is capitalization should be ignored when computing * the distance between two names, and false otherwise. For example, * if the value were true, then the distace between Dallas and dallas would be * zero, but if the value were false, then the distace would be one. * * @return true is capitalization should be ignored when computing * the distance between two names, and false otherwise. */ public boolean isIgnoreCapitalization() { return ignoreCapitalization; } /** * Returns true if punctuation should be ignored when computing the * distance between two names, and false otherwise. The * punctuation characters are defined as follows: * `~!@#$%^&*()_-+=[{]}\|;:'",<.>/? * * @return true if punctuation should be ignored when * computing the distance between two names, and false * otherwise. */ public boolean isIgnorePunctuation() { return ignorePunctuation; } /** * Returns true if whitespace should be ignored when computing the * distance between two names, and false otherwise. If, for * example, the value were true, then the match distance between * LOSANGELES and LOS ANGELES would be one, and if the value were false, * the distance would be zero. * * @return true if whitespace should be ignored when computing the * distance between two names, and false otherwise. */ public boolean isIgnoreWhitespace() { return ignoreWhitespace; } /** * Returns the maximum match level for a match operation. Only cities within a postal * code that meet the maximum match level are considered for inclusion in the results. * Note that CITY_ACTUAL < CITY_ACCEPTABLE < * CITY_UNACCEPTABLE. * * @return the minimum match level for a match operation. */ public int getMaxMatchLevel() { return maxMatchLevel; } } /** * The result of a matching operation. * @author Amin Ahmad */ public static class MatchResult { public int distance; public PostalCodeEntry match; public MatchResult(PostalCodeEntry match, int distance) { super(); this.match = match; this.distance = distance; } public String toString() { return match + ", distance = " + distance; } } }