diff --git a/src/main/java/com/tomaytotomato/SearchLocationService.java b/src/main/java/com/tomaytotomato/SearchLocationService.java index 4a1664a..bdc11c2 100644 --- a/src/main/java/com/tomaytotomato/SearchLocationService.java +++ b/src/main/java/com/tomaytotomato/SearchLocationService.java @@ -29,10 +29,12 @@ public class SearchLocationService implements SearchLocation { /** * 1 to 1 mappings */ + private final Map countryIdToCountryMap = new HashMap<>(); private final Map countryNameToCountryMap = new HashMap<>(); private final Map countryNativeNameToCountry = new HashMap<>(); private final Map iso2CodeToCountryMap = new HashMap<>(); private final Map iso3CodeToCountryMap = new HashMap<>(); + private final Map stateIdToStateMap = new HashMap<>(); /** * 1 to many mappings */ @@ -64,8 +66,8 @@ public SearchLocationService(TextTokeniser textTokeniser, TextNormaliser textNor public void buildDataStructures() { countries.forEach(country -> { - mapCountry(country); - country.getStates().forEach(state -> mapState(state, country)); + buildCountryLookups(country); + country.getStates().forEach(state -> buildStateLookups(state, country)); }); specialMappings(); } @@ -78,6 +80,11 @@ private void specialMappings() { if (ukCountry != null) { mapCountryAliases(ukCountry, "Scotland", "England", "Northern Ireland", "Wales"); iso2CodeToCountryMap.put("uk", ukCountry); + iso2CodeToCountryMap.put("en", ukCountry); + iso3CodeToCountryMap.put("eng", ukCountry); + iso3CodeToCountryMap.put("sco", ukCountry); + iso3CodeToCountryMap.put("wal", ukCountry); + iso3CodeToCountryMap.put("cym", ukCountry); } else { logger.warning( "United Kingdom not found in the country map, unable to add special mappings for Scotland, England, Northern Ireland, and Wales."); @@ -89,8 +96,9 @@ private void specialMappings() { * * @param country The country to be mapped. */ - private void mapCountry(Country country) { + private void buildCountryLookups(Country country) { countryNameToCountryMap.put(keyMaker(country.getName()), country); + countryIdToCountryMap.put(country.getId(), country); if (!Objects.isNull(country.getNativeName()) && !country.getNativeName().isEmpty()) { countryNativeNameToCountry.put(keyMaker(country.getNativeName()), country); } @@ -104,18 +112,19 @@ private void mapCountry(Country country) { * @param state The state to be mapped. * @param country The country the state belongs to. */ - private void mapState(State state, Country country) { + private void buildStateLookups(State state, Country country) { state.setCountryId(country.getId()); state.setCountryName(country.getName()); state.setCountryIso2Code(country.getIso2()); state.setCountryIso3Code(country.getIso3()); + stateIdToStateMap.put(state.getId(), state); stateNameToStatesMap.computeIfAbsent(keyMaker(state.getName()), k -> new ArrayList<>()) .add(state); stateCodeToStatesMap.computeIfAbsent(keyMaker(state.getStateCode()), k -> new ArrayList<>()) .add(state); - state.getCities().forEach(city -> mapCity(city, state, country)); + state.getCities().forEach(city -> buildCityLookups(city, state, country)); } /** @@ -125,7 +134,7 @@ private void mapState(State state, Country country) { * @param state The state the city belongs to. * @param country The country the city belongs to. */ - private void mapCity(City city, State state, Country country) { + private void buildCityLookups(City city, State state, Country country) { city.setCountryId(country.getId()); city.setCountryName(country.getName()); city.setCountryIso2Code(country.getIso2()); @@ -232,9 +241,9 @@ private List findDirectMatches(String text) { * @return A list of matching locations. */ private List findTokenizedMatches(List tokenizedText) { - Map countryHitsCount = new HashMap<>(); - Map stateHitsCount = new HashMap<>(); - Map cityHitsCount = new HashMap<>(); + Map countryHitsCount = new HashMap<>(); + Map stateHitsCount = new HashMap<>(); + Map cityHitsCount = new HashMap<>(); boolean countryFound = populateCountryHits(tokenizedText, countryHitsCount); @@ -256,18 +265,20 @@ private List findTokenizedMatches(List tokenizedText) { * @return true if a country is found, false otherwise. */ private boolean populateCountryHits(List tokenizedText, - Map countryHitsCount) { + Map countryHitsCount) { Iterator iterator = tokenizedText.iterator(); while (iterator.hasNext()) { String token = iterator.next(); if (countryNameToCountryMap.containsKey(token)) { - countryHitsCount.put(token, countryHitsCount.getOrDefault(token, 0) + 1); + var country = countryNameToCountryMap.get(token); + countryHitsCount.put(country, countryHitsCount.getOrDefault(country, 0) + 1); iterator.remove(); return true; } if (iso3CodeToCountryMap.containsKey(token)) { - countryHitsCount.put(token, countryHitsCount.getOrDefault(token, 0) + 1); + var country = iso3CodeToCountryMap.get(token); + countryHitsCount.put(country, countryHitsCount.getOrDefault(country, 0) + 1); iterator.remove(); return true; } @@ -284,31 +295,39 @@ private boolean populateCountryHits(List tokenizedText, * @param cityHitsCount The map to populate with city hits. */ private void populateStateAndCityHits(List tokenizedText, - Map countryHitsCount, - Map stateHitsCount, Map cityHitsCount) { + Map countryHitsCount, + Map stateHitsCount, + Map cityHitsCount) { + + var stateFound = false; + var cityFound = false; + for (String token : tokenizedText) { - if (stateNameToStatesMap.containsKey(token)) { + if (stateNameToStatesMap.containsKey(token) && !stateFound) { stateNameToStatesMap.get(token).forEach(state -> { - stateHitsCount.put(state.getName(), stateHitsCount.getOrDefault(state.getName(), 0) + 1); - countryHitsCount.put(state.getCountryName(), - countryHitsCount.getOrDefault(state.getCountryName(), 0) + 1); + var country = countryIdToCountryMap.get(state.getCountryId()); + stateHitsCount.put(state, stateHitsCount.getOrDefault(state, 0) + 1); + countryHitsCount.put(country, countryHitsCount.getOrDefault(country, 0) + 1); }); + stateFound = true; } - if (stateCodeToStatesMap.containsKey(token)) { + if (stateCodeToStatesMap.containsKey(token) && !stateFound) { stateCodeToStatesMap.get(token).forEach(state -> { - stateHitsCount.put(state.getName(), stateHitsCount.getOrDefault(state.getName(), 0) + 1); - countryHitsCount.put(state.getCountryName(), - countryHitsCount.getOrDefault(state.getCountryName(), 0) + 1); + var country = countryIdToCountryMap.get(state.getCountryId()); + stateHitsCount.put(state, stateHitsCount.getOrDefault(state, 0) + 1); + countryHitsCount.put(country, countryHitsCount.getOrDefault(country, 0) + 1); }); + stateFound = true; } - if (cityNameToCitiesMap.containsKey(token)) { + if (cityNameToCitiesMap.containsKey(token) && !cityFound) { cityNameToCitiesMap.get(token).forEach(city -> { - cityHitsCount.put(city.getName(), cityHitsCount.getOrDefault(city.getName(), 0) + 1); - stateHitsCount.put(city.getStateName(), - stateHitsCount.getOrDefault(city.getStateName(), 0) + 1); - countryHitsCount.put(city.getCountryName(), - countryHitsCount.getOrDefault(city.getCountryName(), 0) + 1); + var country = countryIdToCountryMap.get(city.getCountryId()); + var state = stateIdToStateMap.get(city.getStateId()); + stateHitsCount.put(state, stateHitsCount.getOrDefault(state, 0) + 1); + cityHitsCount.put(city, cityHitsCount.getOrDefault(city, 0) + 1); + countryHitsCount.put(country, countryHitsCount.getOrDefault(country, 0) + 1); }); + cityFound = true; } } } @@ -322,31 +341,38 @@ private void populateStateAndCityHits(List tokenizedText, * @param cityHitsCount The map to populate with city hits. */ private void filterAndPopulateStateAndCityHits(List tokenizedText, - Map countryHitsCount, - Map stateHitsCount, Map cityHitsCount) { - String topCountry = getTopCountry(countryHitsCount); + Map countryHitsCount, + Map stateHitsCount, + Map cityHitsCount) { + var topCountry = getTopCountry(countryHitsCount); + var stateFound = false; + var cityFound = false; for (String token : tokenizedText) { - if (stateNameToStatesMap.containsKey(token)) { + if (stateNameToStatesMap.containsKey(token) && !stateFound) { stateNameToStatesMap.get(token).stream() - .filter(state -> state.getCountryName().equals(topCountry)) - .forEach(state -> stateHitsCount.put(state.getName(), - stateHitsCount.getOrDefault(state.getName(), 0) + 1)); + .filter(state -> state.getCountryName().equals(topCountry.getName())) + .forEach(state -> stateHitsCount.put(state, + stateHitsCount.getOrDefault(state, 0) + 1)); + stateFound = true; } - if (stateCodeToStatesMap.containsKey(token)) { + if (stateCodeToStatesMap.containsKey(token) && !stateFound) { stateCodeToStatesMap.get(token).stream() - .filter(state -> state.getCountryName().equals(topCountry)) - .forEach(state -> stateHitsCount.put(state.getName(), - stateHitsCount.getOrDefault(state.getName(), 0) + 1)); + .filter(state -> state.getCountryName().equals(topCountry.getName())) + .forEach(state -> stateHitsCount.put(state, + stateHitsCount.getOrDefault(state, 0) + 1)); + stateFound = true; } - if (cityNameToCitiesMap.containsKey(token)) { + if (cityNameToCitiesMap.containsKey(token) && !cityFound) { cityNameToCitiesMap.get(token).stream() - .filter(city -> city.getCountryName().equals(topCountry)) + .filter(city -> city.getCountryName().equals(topCountry.getName())) .forEach(city -> { - cityHitsCount.put(city.getName(), cityHitsCount.getOrDefault(city.getName(), 0) + 1); - stateHitsCount.put(city.getStateName(), - stateHitsCount.getOrDefault(city.getStateName(), 0) + 1); + var state = stateIdToStateMap.get(city.getStateId()); + cityHitsCount.put(city, cityHitsCount.getOrDefault(city, 0) + 1); + stateHitsCount.put(state, + stateHitsCount.getOrDefault(state, 0) + 1); }); + cityFound = true; } } } @@ -359,28 +385,35 @@ private void filterAndPopulateStateAndCityHits(List tokenizedText, * @param cityHitsCount The map of city hits. * @return A list of top matching locations. */ - private List getTopMatchingLocations(Map countryHitsCount, - Map stateHitsCount, - Map cityHitsCount) { + private List getTopMatchingLocations(Map countryHitsCount, + Map stateHitsCount, + Map cityHitsCount) { + var topCountry = getTopCountry(countryHitsCount); - var topState = getTopHit(stateHitsCount); - if (Objects.isNull(topState)) { - return List.of(locationMapper.toLocation(countryNameToCountryMap.get(topCountry))); - } + var topCity = cityHitsCount.entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse(null); - var topCity = getTopHit(cityHitsCount); + var topState = stateHitsCount.entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse(null); - if (!Objects.isNull(topCity) && !Objects.isNull(topState) && !Objects.isNull(topCountry)) { - var cityMatches = cityNameToCitiesMap.get(topCity.toLowerCase()); - for (City city : cityMatches) { - if (city.getStateName().equals(topState) && city.getCountryName().equals(topCountry)) { - return List.of(locationMapper.toLocation(city)); + if (Objects.isNull(topCountry)) { + if (Objects.isNull(topState)) { + if (Objects.isNull(topCity)) { + return List.of(); + } else { + return List.of(locationMapper.toLocation(topCity)); } + } else { + return List.of(locationMapper.toLocation(topState)); } + } else { + return List.of(locationMapper.toLocation(topCountry)); } - - return List.of(); } /** @@ -389,23 +422,10 @@ private List getTopMatchingLocations(Map countryHitsC * @param countryHitsCount The map of country hits. * @return The top country name. */ - private String getTopCountry(Map countryHitsCount) { + private Country getTopCountry(Map countryHitsCount) { return countryHitsCount.entrySet().stream() .max(Map.Entry.comparingByValue()) .map(Map.Entry::getKey) .orElse(null); } - - /** - * Gets the top hit from the given hit count map. - * - * @param hitsCount The map of hits. - * @return The top hit name. - */ - private String getTopHit(Map hitsCount) { - return hitsCount.entrySet().stream() - .max(Map.Entry.comparingByValue()) - .map(Map.Entry::getKey) - .orElse(null); - } } diff --git a/src/main/java/com/tomaytotomato/model/Location.java b/src/main/java/com/tomaytotomato/model/Location.java index 49ec2e6..aa66f62 100644 --- a/src/main/java/com/tomaytotomato/model/Location.java +++ b/src/main/java/com/tomaytotomato/model/Location.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator.Mode; import java.math.BigDecimal; -import java.util.Objects; public class Location { @@ -14,6 +13,7 @@ public class Location { private String state; private Integer stateId; private String stateCode; + private String stateName; private String city; private Integer cityId; private BigDecimal latitude; @@ -24,7 +24,7 @@ public Location() { @JsonCreator(mode = Mode.DISABLED) public Location(String countryName, Integer countryId, String countryIso2Code, - String countryIso3Code, String state, Integer stateId, String stateCode, String city, + String countryIso3Code, String state, Integer stateId, String stateCode, String stateName, String city, Integer cityId, BigDecimal latitude, BigDecimal longitude) { this.countryName = countryName; this.countryId = countryId; @@ -33,16 +33,13 @@ public Location(String countryName, Integer countryId, String countryIso2Code, this.state = state; this.stateId = stateId; this.stateCode = stateCode; + this.stateName = stateName; this.city = city; this.cityId = cityId; this.latitude = latitude; this.longitude = longitude; } - public static Builder builder() { - return new Builder(); - } - public String getCountryName() { return countryName; } @@ -99,6 +96,14 @@ public void setStateCode(String stateCode) { this.stateCode = stateCode; } + public String getStateName() { + return stateName; + } + + public void setStateName(String stateName) { + this.stateName = stateName; + } + public String getCity() { return city; } @@ -131,50 +136,8 @@ public void setLongitude(BigDecimal longitude) { this.longitude = longitude; } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Location location = (Location) o; - return Objects.equals(getCountryName(), location.getCountryName()) - && Objects.equals(getCountryId(), location.getCountryId()) - && Objects.equals(getCountryIso2Code(), location.getCountryIso2Code()) - && Objects.equals(getCountryIso3Code(), location.getCountryIso3Code()) - && Objects.equals(getState(), location.getState()) && Objects.equals( - getStateId(), location.getStateId()) && Objects.equals(getStateCode(), - location.getStateCode()) && Objects.equals(getCity(), location.getCity()) - && Objects.equals(getCityId(), location.getCityId()) && Objects.equals( - getLatitude(), location.getLatitude()) && Objects.equals(getLongitude(), - location.getLongitude()); - } - - @Override - public int hashCode() { - return Objects.hash(getCountryName(), getCountryId(), getCountryIso2Code(), - getCountryIso3Code(), - getState(), getStateId(), getStateCode(), getCity(), getCityId(), getLatitude(), - getLongitude()); - } - - @Override - public String toString() { - return "Location{" + - "countryName='" + countryName + '\'' + - ", countryId=" + countryId + - ", countryIso2Code='" + countryIso2Code + '\'' + - ", countryIso3Code='" + countryIso3Code + '\'' + - ", state='" + state + '\'' + - ", stateId=" + stateId + - ", stateCode='" + stateCode + '\'' + - ", city='" + city + '\'' + - ", cityId=" + cityId + - ", latitude=" + latitude + - ", longitude=" + longitude + - '}'; + public static Builder builder() { + return new Builder(); } public static class Builder { @@ -186,6 +149,7 @@ public static class Builder { private String state; private Integer stateId; private String stateCode; + private String stateName; private String city; private Integer cityId; private BigDecimal latitude; @@ -228,6 +192,11 @@ public Builder stateCode(String stateCode) { return this; } + public Builder stateName(String stateName) { + this.stateName = stateName; + return this; + } + public Builder city(String city) { this.city = city; return this; @@ -250,7 +219,7 @@ public Builder longitude(BigDecimal longitude) { public Location build() { return new Location(countryName, countryId, countryIso2Code, countryIso3Code, state, stateId, - stateCode, city, cityId, latitude, longitude); + stateCode, stateName, city, cityId, latitude, longitude); } } diff --git a/src/test/java/com/tomaytotomato/usecase/SearchLocationTest.java b/src/test/java/com/tomaytotomato/usecase/SearchLocationTest.java index cf07630..2fcd9df 100644 --- a/src/test/java/com/tomaytotomato/usecase/SearchLocationTest.java +++ b/src/test/java/com/tomaytotomato/usecase/SearchLocationTest.java @@ -120,9 +120,16 @@ void search_WhenTextContainsCountryISO3CodeOnly_ThenReturnCountryMatch(String is "Santa Clara CA, United States", "Glasgow Scotland, United Kingdom", "Tel Aviv Israel, Israel", + "Tel Aviv ISR, Israel", "Germany Saxony, Germany", "France, France", - "England, United Kingdom" + "England, United Kingdom", + "Gloucester ENG, United Kingdom", + "Eng Sheffield, United Kingdom", + "United Kingdom . Sheffield, United Kingdom", + "Tacoma United States, United States", + "Tacoma, United States", + "USA Tacoma, United States", }) void search_WhenTextContainsStateAndCountryName_ThenReturnSingleMatch(String text, String countryName) {