suumo-search

Perform advanced searches on Suumo.jp
git clone https://git.neuralcrash.com/suumo-search.git
Log | Files | Refs | README

commit d1752ada1e84bdf02d0e251c7d0b4e5ce2134361
parent a287fa62af88f84cc56e85f5672da60d94c70a68
Author: Kebigon <git@kebigon.xyz>
Date:   Sun,  5 Apr 2020 18:29:26 +0900

Add fastest/cheapest/easiest route function to use in expressions
Diffstat:
Msrc/main/java/xyz/kebigon/housesearch/domain/SearchConditionsValidator.java | 284++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/main/packaged-resources/cfg/cheap-house-expression.cfg | 8+++-----
Msrc/main/packaged-resources/cfg/searches.json | 2+-
Msrc/main/packaged-resources/cfg/tokyo-house-expression.cfg | 25+++++++++++++++----------
4 files changed, 170 insertions(+), 149 deletions(-)

diff --git a/src/main/java/xyz/kebigon/housesearch/domain/SearchConditionsValidator.java b/src/main/java/xyz/kebigon/housesearch/domain/SearchConditionsValidator.java @@ -1,5 +1,9 @@ package xyz.kebigon.housesearch.domain; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; + import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -13,137 +17,151 @@ import xyz.kebigon.housesearch.ApplicationContext; @Slf4j public class SearchConditionsValidator { - public static boolean validateBasicConditions(Posting posting, SearchConditions conditions) - { - final String url = posting.getUrl(); - if (url == null) - { - log.debug("Missing url for {}", posting); - return false; - } - - final Long price = posting.getPrice(); - if (price == null) - { - log.debug("Missing price for {}", posting); - return false; - } - if (conditions.getMinPrice() != null && price < conditions.getMinPrice()) - return false; - if (conditions.getMaxPrice() != null && price > conditions.getMaxPrice()) - return false; - - final Integer age = posting.getAge(); - if (age == null) - { - log.debug("Missing age for {}", posting); - return false; - } - if (conditions.getMinAge() != null && age < conditions.getMinAge()) - return false; - if (conditions.getMaxAge() != null && age > conditions.getMaxAge()) - return false; - - final Double landSurface = posting.getLandSurface(); - if (landSurface == null) - { - log.debug("Missing landSurface for {}", posting); - return false; - } - if (conditions.getMinLandSurface() != null && landSurface < conditions.getMinLandSurface()) - return false; - if (conditions.getMaxLandSurface() != null && landSurface > conditions.getMaxLandSurface()) - return false; - - final Double houseSurface = posting.getHouseSurface(); - if (houseSurface == null) - { - log.debug("Missing houseSurface for {}", posting); - return false; - } - if (conditions.getMinHouseSurface() != null && houseSurface < conditions.getMinHouseSurface()) - return false; - if (conditions.getMaxHouseSurface() != null && houseSurface > conditions.getMaxHouseSurface()) - return false; - - final Integer walkTimeToStation = posting.getWalkTimeToStation(); - if (walkTimeToStation == null) - { - log.debug("Missing walkTimeToStation for {}", posting); - return false; - } - if (conditions.getMaxWalkTimeToStation() != null && walkTimeToStation > conditions.getMaxWalkTimeToStation()) - return false; - - final String station = posting.getStation(); - if (station == null) - { - log.debug("Missing station for {}", posting); - return false; - } - - return true; - } - - public static boolean validateExpression(Posting posting, SearchConditions conditions) - { - final ExpressionParser expressionParser = new SpelExpressionParser(); - - final StandardEvaluationContext context = new StandardEvaluationContext(); - context.setVariable("property", posting); - - try - { - context.registerFunction("timeToStation", - SearchConditionsValidator.class.getDeclaredMethod("timeToStation", new Class[] { Posting.class, String.class })); - context.registerFunction("fareToStation", - SearchConditionsValidator.class.getDeclaredMethod("fareToStation", new Class[] { Posting.class, String.class })); - context.registerFunction("transferToStation", - SearchConditionsValidator.class.getDeclaredMethod("transferToStation", new Class[] { Posting.class, String.class })); - } - catch (NoSuchMethodException | SecurityException e) - { - e.printStackTrace(); - throw new RuntimeException(e); - } - - final Expression expression = expressionParser.parseExpression(conditions.getExpression()); - return expression.getValue(context, posting, Boolean.class); - } - - @SuppressWarnings("unused") - private static int timeToStation(Posting posting, String station) - { - final Route route = ApplicationContext.getYahooTransitBrowser() // - .search(posting.getStation(), station).stream() // - .min(Route.TIME_COMPARATOR).orElse(Route.IMPOSSIBLE_ROUTE); - - posting.updateRoutes(route); - - return route.getTime() + posting.getWalkTimeToStation(); - } - - @SuppressWarnings("unused") - private static int fareToStation(Posting posting, String station) - { - final Route route = ApplicationContext.getYahooTransitBrowser() // - .search(posting.getStation(), station).stream() // - .min(Route.FARE_COMPARATOR).orElse(Route.IMPOSSIBLE_ROUTE); - - posting.updateRoutes(route); - - return route.getFare(); - } - - @SuppressWarnings("unused") - private static int transferToStation(Posting posting, String station) - { - final Route route = ApplicationContext.getYahooTransitBrowser() // - .search(posting.getStation(), station).stream() // - .min(Route.TRANSFER_COMPARATOR).orElse(Route.IMPOSSIBLE_ROUTE); - - posting.updateRoutes(route); - - return route.getTransfer(); - } + public static boolean validateBasicConditions(Posting posting, SearchConditions conditions) + { + final String url = posting.getUrl(); + if (url == null) + { + log.debug("Missing url for {}", posting); + return false; + } + + final Long price = posting.getPrice(); + if (price == null) + { + log.debug("Missing price for {}", posting); + return false; + } + if (conditions.getMinPrice() != null && price < conditions.getMinPrice()) + return false; + if (conditions.getMaxPrice() != null && price > conditions.getMaxPrice()) + return false; + + final Integer age = posting.getAge(); + if (age == null) + { + log.debug("Missing age for {}", posting); + return false; + } + if (conditions.getMinAge() != null && age < conditions.getMinAge()) + return false; + if (conditions.getMaxAge() != null && age > conditions.getMaxAge()) + return false; + + final Double landSurface = posting.getLandSurface(); + if (landSurface == null) + { + log.debug("Missing landSurface for {}", posting); + return false; + } + if (conditions.getMinLandSurface() != null && landSurface < conditions.getMinLandSurface()) + return false; + if (conditions.getMaxLandSurface() != null && landSurface > conditions.getMaxLandSurface()) + return false; + + final Double houseSurface = posting.getHouseSurface(); + if (houseSurface == null) + { + log.debug("Missing houseSurface for {}", posting); + return false; + } + if (conditions.getMinHouseSurface() != null && houseSurface < conditions.getMinHouseSurface()) + return false; + if (conditions.getMaxHouseSurface() != null && houseSurface > conditions.getMaxHouseSurface()) + return false; + + final Integer walkTimeToStation = posting.getWalkTimeToStation(); + if (walkTimeToStation == null) + { + log.debug("Missing walkTimeToStation for {}", posting); + return false; + } + if (conditions.getMaxWalkTimeToStation() != null && walkTimeToStation > conditions.getMaxWalkTimeToStation()) + return false; + + final String station = posting.getStation(); + if (station == null) + { + log.debug("Missing station for {}", posting); + return false; + } + + return true; + } + + public static boolean validateExpression(Posting posting, SearchConditions conditions) + { + final ExpressionParser expressionParser = new SpelExpressionParser(); + + final StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("property", posting); + + try + { + context.registerFunction("fastestRoute", + SearchConditionsValidator.class.getDeclaredMethod("getFastestRoute", new Class[] { Posting.class, String[].class })); + context.registerFunction("cheapestRoute", + SearchConditionsValidator.class.getDeclaredMethod("getCheapestRoute", new Class[] { Posting.class, String[].class })); + context.registerFunction("easiestRoute", + SearchConditionsValidator.class.getDeclaredMethod("getEasiestRoute", new Class[] { Posting.class, String[].class })); + + context.registerFunction("timeToStation", + SearchConditionsValidator.class.getDeclaredMethod("timeToStation", new Class[] { Posting.class, String.class })); + context.registerFunction("fareToStation", + SearchConditionsValidator.class.getDeclaredMethod("fareToStation", new Class[] { Posting.class, String.class })); + context.registerFunction("transferToStation", + SearchConditionsValidator.class.getDeclaredMethod("transferToStation", new Class[] { Posting.class, String.class })); + } + catch (NoSuchMethodException | SecurityException e) + { + e.printStackTrace(); + throw new RuntimeException(e); + } + + final Expression expression = expressionParser.parseExpression(conditions.getExpression()); + return expression.getValue(context, posting, Boolean.class); + } + + private static Stream<Route> getRoutes(Posting posting, String... stations) + { + return Arrays.stream(stations) // + .flatMap(station -> { + final Collection<Route> routes = ApplicationContext.getYahooTransitBrowser().search(posting.getStation(), station); + routes.forEach(posting::updateRoutes); + return routes.stream(); + }); + } + + private static Route getFastestRoute(Posting posting, String... stations) + { + return getRoutes(posting, stations).min(Route.TIME_COMPARATOR).orElse(Route.IMPOSSIBLE_ROUTE); + } + + private static Route getCheapestRoute(Posting posting, String... stations) + { + return getRoutes(posting, stations).min(Route.FARE_COMPARATOR).orElse(Route.IMPOSSIBLE_ROUTE); + } + + private static Route getEasiestRoute(Posting posting, String... stations) + { + return getRoutes(posting, stations).min(Route.TRANSFER_COMPARATOR).orElse(Route.IMPOSSIBLE_ROUTE); + } + + @SuppressWarnings("unused") + private static int timeToStation(Posting posting, String station) + { + return getFastestRoute(posting, station).getTime() + posting.getWalkTimeToStation(); + } + + @SuppressWarnings("unused") + private static int fareToStation(Posting posting, String station) + { + return getCheapestRoute(posting, station).getFare(); + } + + @SuppressWarnings("unused") + private static int transferToStation(Posting posting, String station) + { + return getEasiestRoute(posting, station).getTransfer(); + } } diff --git a/src/main/packaged-resources/cfg/cheap-house-expression.cfg b/src/main/packaged-resources/cfg/cheap-house-expression.cfg @@ -1,10 +1,8 @@ -# At least one good route ( - (#timeToStation(#property, '東京駅') <= 85 && #fareToStation(#property, '東京駅') <= 650 && #transferToStation(#property, '東京駅') <= 1) - || (#timeToStation(#property, '三越前') <= 85 && #fareToStation(#property, '三越前') <= 650 && #transferToStation(#property, '三越前') <= 1) - || (#timeToStation(#property, '大手町(東京都)駅') <= 85 && #fareToStation(#property, '大手町(東京都)駅') <= 650 && #transferToStation(#property, '大手町(東京都)駅') <= 1) + (#fastestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').time + #property.walkTimeToStation) <= 80 + && #fastestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').fare <= 650 + && #fastestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').transfer <= 1 ) -# Mistaken with Tokyo's Akasaka && !url.startsWith('https://suumo.jp/chukoikkodate/gumma/sc_maebashi') \ No newline at end of file diff --git a/src/main/packaged-resources/cfg/searches.json b/src/main/packaged-resources/cfg/searches.json @@ -4,7 +4,7 @@ "area": "KANTO", "type": "USED_HOUSE", "price.min": null, - "price.max": 12000000, + "price.max": 11000000, "age.min": null, "age.max": 20, "surface.land.min": 100, diff --git a/src/main/packaged-resources/cfg/tokyo-house-expression.cfg b/src/main/packaged-resources/cfg/tokyo-house-expression.cfg @@ -3,22 +3,27 @@ ( # Fast route - (#timeToStation(#property, '東京駅') <= 40 && #fareToStation(#property, '東京駅') <= 750 && #transferToStation(#property, '東京駅') <= 1) - || (#timeToStation(#property, '三越前') <= 40 && #fareToStation(#property, '三越前') <= 750 && #transferToStation(#property, '三越前') <= 1) - || (#timeToStation(#property, '大手町(東京都)駅') <= 40 && #fareToStation(#property, '大手町(東京都)駅') <= 750 && #transferToStation(#property, '大手町(東京都)駅') <= 1) + ( + (#fastestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').time + #property.walkTimeToStation) <= 40 + && #fastestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').fare <= 750 + && #fastestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').transfer <= 1 + ) # Cheap route - || (#timeToStation(#property, '東京駅') <= 60 && #fareToStation(#property, '東京駅') <= 500 && #transferToStation(#property, '東京駅') <= 1) - || (#timeToStation(#property, '三越前') <= 60 && #fareToStation(#property, '三越前') <= 500 && #transferToStation(#property, '三越前') <= 1) - || (#timeToStation(#property, '大手町(東京都)駅') <= 60 && #fareToStation(#property, '大手町(東京都)駅') <= 500 && #transferToStation(#property, '大手町(東京都)駅') <= 1) + || ( + (#cheapestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').time + #property.walkTimeToStation) <= 60 + && #cheapestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').fare <= 500 + && #cheapestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').transfer <= 1 + ) # Direct route - || (#timeToStation(#property, '東京駅') <= 60 && #fareToStation(#property, '東京駅') <= 750 && #transferToStation(#property, '東京駅') == 0) - || (#timeToStation(#property, '三越前') <= 60 && #fareToStation(#property, '三越前') <= 750 && #transferToStation(#property, '三越前') == 0) - || (#timeToStation(#property, '大手町(東京都)駅') <= 60 && #fareToStation(#property, '大手町(東京都)駅') <= 750 && #transferToStation(#property, '大手町(東京都)駅') == 0) + || ( + (#easiestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').time + #property.walkTimeToStation) <= 60 + && #easiestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').fare <= 750 + && #easiestRoute(#property, '東京駅', '三越前', '大手町(東京都)駅').transfer == 0 + ) ) -# Mistaken with Tokyo's Akasaka && !url.startsWith('https://suumo.jp/chukoikkodate/gumma/sc_maebashi') \ No newline at end of file