2
\$\begingroup\$

Given a URL like this:

localhost:3000/sell-residential/house-apartment-penthouse-size-30-any-for-0-100000-in-miami%3b+orlando%3b/list-1?numRooms=2&numBaths=2&numParkingSpaces=1&misc=photo-only&interiorFeatures=heating%2Clift&exteriorFeatures=garden%2Cbalcony

I would like to extract some values using RegEx. What I have done is the following fully-commented function:

function listingTokens(reqUrl){

    let retValue = {page: LISTING_PAGE_URI};

    const path_parameters_matcher = /(?:([^?]+))/g;
    // Group1 = url before query string
    // Group2 = url after query string
    let match = reqUrl.match(path_parameters_matcher);

    let path = match[0];
    let params = match[1];

    const path_matcher = /(?:([^\/\/]+))/g;
    // get the path tokens separed by /
    let path_tokens = path.match(path_matcher);

    let filter_values = [];
    // first path token --> Mandatory gives the channel and the category
    let channel_category = path_tokens[0];
    // second path token --> Mandatory gives main filter values
    let filters = path_tokens[1];
    // third path token --> Mandatory page requested
    let list_page = reqUrl.match(page_matcher)[1];

    // exctract channel and category splitting the previous extracted token
    let channel = channel_category.split('-')[0];
    let category = channel_category.split('-').length > 1 ? channel_category.split('-')[1] : '';

    // default filters object
    let filtersObj = {
        "channel": channel || null,
        "category_type": category || null,
        "property_type": [],
        "localities": [],
        "min_price": "any",
        "max_price": "any",
        "min_meter": "any",
        "max_meter": "any",
        "bedroom_number": "any"
    };

    // Regular expressions for match the filter values considering random order and possible missing path values
    let property_filters_matcher = /(?=house-).*(?=-size-)|(?=house-).*(?=-for-)|(?=house-).*(?=-in-)|(?=house-).*/;
    let size_filters_matcher = /(?=size-).*(?=-house-)|(?=size-).*(?=-for-)|(?=size-).*(?=-in-)|(?=size-).*/;
    let price_filters_matcher = /(?=for-).*(?=-size-)|(?=for-).*(?=-house-)|(?=for-).*(?=-in-)|(?=for-).*/;
    let locality_filters_matcher = /(?=in-).*(?=-size-)|(?=in-).*(?=-for-)|(?=in-).*(?=-house-)|(?=in-).*/;
    // Regular expressions for match tokens separed by -
    const tokens_matcher = /(?:([^-]+))/g;

    // Should match for ex. house-apartment-penthouse
    match = property_filters_matcher.exec(filters);
    if(match){
        // the first token would be the filter name and the spread result would be the values
        let [filter, ...values] = match[0].match(tokens_matcher);
        filtersObj.property_type = values;
    }
    // Should match for ex. size-30-any
    match = size_filters_matcher.exec(filters);
    if(match){
        // the first token would be the filter name the second the from size and the third to size
        let [filter, from, to] = match[0].match(tokens_matcher);
        filtersObj.min_meter = from;
        filtersObj.max_meter = to;
    }
    // Should match for ex. for-0-100000
    match = price_filters_matcher.exec(filters);
    if(match){
        // the first token would be the filter name the second the from price and the third to price
        let [filter, from, to] = match[0].match(tokens_matcher);
        filtersObj.min_price = from;
        filtersObj.max_price = to;
    }
    // Should match for ex. in-miami%3b+orlando%3b
    match = locality_filters_matcher.exec(filters);
    if(match){
        // the first token would be the filter name the second the locality or localities
        let [filter, value] = match[0].match(tokens_matcher);
        // clean the values and split by separator ; (%3b)
        let local = value.replace("+", "").split(';');
        let localities = [];
        for(let i = 0; i < local.length; i++){
            // if not empty add to the result
            if(local[i]){
                localities.push({"key": local[i], "displayValue": local[i].replace(",", " "), "withZones": false});
            }
        }

        filtersObj.localities = localities;
    }

    // Merge the objects in the final result
    Object.assign(retValue, {pageNumber: list_page, selected_filters: filtersObj});
    return retValue;
}

I'm not handling the possible query string values by now, but I think it would be something similar to the filters matching.

What I am asking is if there is a better way to extract those values -- I'm confident there is, because I'm pretty new in ES2015 and advanced RegExes are not my strongest field of knowledge.

\$\endgroup\$
  • \$\begingroup\$ The line "const path_matcher = /(?:([^\/\/]+))/g;" : Wouldn't have '/(?:([^\/]+))/g' the same result. You add the slash ( / ) two times to your set of not allowed characters. The second time isn't necessary. At least I guess so? I can be wrong ... \$\endgroup\$ – michael.zech Apr 6 '16 at 7:11
  • \$\begingroup\$ Have you looked into a library to do this? It seems like it would be more effective than reinventing your own. I found gist.github.com/jlong/2428561 and npmjs.com/package/url-parse in the first few search results. Even if you can't use them, you can look over the code base and see if anything can be used with what you have. The second link specifically mentions a RegEx engine. \$\endgroup\$ – Zack Apr 12 '16 at 14:16
  • \$\begingroup\$ Even without npm, there's URL parsing built into Node already, so there's no need to roll your own \$\endgroup\$ – Flambino Apr 12 '16 at 15:33
  • \$\begingroup\$ @Flambino I know there is URL parsing in Node, but if you read the code you will notice that my requirement is a little far from parsing the url, I need to exctract specific values separated in different ways \$\endgroup\$ – Daniele May 5 '16 at 10:19
2
\$\begingroup\$

I can give you some feedback on your regexes.

First of all, you don't need the parenthesis in some of these. These two:

/(?:([^?]+))/g
/(?:([^-]+))/g

would become:

/[^?]+/g
/[^-]+/g

The first capture group was equal to the entire matched group, so you just use [0] (which you appear to be doing already, except for the one line).

Now the same thing applies to this regex too, but you can also remove one set of \/s:

/(?:([^\/\/]+))/g

becomes:

/[^\/]+/g

Next, there's this regex, and two others like it. I have added spaces so you can see the repetition:

/(?=house-).*(?=-size-)|
 (?=house-).*(?=-for-) |
 (?=house-).*(?=-in-)  |
 (?=house-).*/

As it is written, the regex takes 427 steps to find a match in your example string (according to regex101.com). There's no compelling reason to be using look aheads for "house-" here, either. It will just be eaten by by the .*, so you might as well just extract it:

/house-(?:.*(?=-size-)|.*(?=-for-)|.*(?=-in-)|.*)/

This cuts it down to 391 steps, but it is still getting hung up in the .* portion.

Looking further into your code, it seems that you just want to get the two values after it: house-???????-???????-. Your other regex already does that, so you can just use it here, instead. The other thing I would suggest is using \b, which ensures that there's a word boundary (so the following will not match something starting with "penthouse"):

/\bhouse-([^-]+)-([^-]+)/

This cuts it down to 18 steps. That's a HUGE improvement from the original 427. It's also shorter and more readable than your original.

You can get the matches by using:

match = property_filters_matcher.exec(filters);
if(match){
    filtersObj.min_meter = match[1];
    filtersObj.max_meter = match[2];
}

This will allow the regex to be more robust, and also improve some of the other parts of your code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.