3

I am using a 3rd party API that allows me to search for housing properties. Unfortunately the API is not written in a way to allow me to search for a range so I have to make a separate call for each value in the range.

So if I want to search for all the housing properties that have 2 or 3 bedrooms I would have to make call for 2 bedrooms, then another call for 3 bedrooms. Now this can get quite tricky as there are multiple fields that can contain a range of numbers (bedrooms, bathroom, floors, garage size...).

My brute force JavaScript solution for this is to create a nested for loop that will create an array of all the calls. This is not a scalable solution and I'm looking for a way to dynamically create this for loop or another alternative way to get an array of all my calls.

My current solution:

const searchParams = {
    minBedrooms: 2,
    maxBedrooms: 4,
    minBathrooms: 1,
    maxBathrooms: 3,
    minFloors: 1,
    maxFloors: 1
};

let promises = [];

for (let bedrooms = searchParams.minBedrooms; bedrooms <= searchParams.maxBedrooms; bedrooms++) {
    for (let bathrooms = searchParams.minBathrooms; bathrooms <= searchParams.maxBathrooms; bathrooms++) {
        for (let floors = searchParams.minFloors; floors <= searchParams.maxFloors; floors++) {
            promises.push(callApi(bedrooms, bathrooms, floors));
        }
    }
}

Promise.all(promises).then(response => {
    // do stuff with response
}

Furthermore the user might not specify one of the search parameters (ie - number of bedrooms). As a result, the API will not apply that specific filter. My code currently will fail no bedroom values are passed in, and writing condition statements for each for loop is not a desire of mine.

Any ideas on how to dynamically generate the above nested for loop?


EDIT

My current solution will fail if the user does not specify the number of bedrooms but specifies bathrooms/floors as the initial for loop will not get run. I don't want to resort to using condition statements along with lots of nested loops to be creating my promise array. This is why I feel like I need to use a dynamically generated for loop.

12
  • I am using Promise.all(promises).then(response => ...);. I will update my code with this. Commented Apr 21, 2018 at 3:24
  • CertainPerformance says, you must use [] (Array), not {}(Object). Commented Apr 21, 2018 at 3:27
  • @CertainPerformance woops, typo. Good catch :) Ty Isitea for explaining Commented Apr 21, 2018 at 3:28
  • If you want to check all combinations of # of bathrooms, # of bedrooms, and # of floors, you cannot do any better than the nested loops you have here unless the API provides a way to pass these parameters. Commented Apr 21, 2018 at 3:32
  • Also look into bluebird promise's concurrency options. You will want to limit the concurrency. 100 simultaneous requests will cause them all to fail. Doing 25 at a time will likely succeed, but will take more time. Doing 8 at a time is a good start. Play around with this, and keep an eye on timing and failure rates. Commented Apr 21, 2018 at 3:37

4 Answers 4

6

One way to look at this is called a Cartesian product A × B × C -- for every a in A and b in B and c in C, you want a tuple (a, b, c).

For example {1, 2} × {3, 4} has 4 resulting tuples: (1, 3), (1, 4), (2, 3), (2, 4).

The easiest way to produce this is to start with just the options in the first set: (1) and (2). Then, for each option in the second set, complete each tuple with the new value:

  • (1), (2) with 3 added gets (1, 3) and (2, 3)
  • (1), (2) with 4 added gets (1, 4) and (2, 4)

In code, this might look like this:

// Each key is associated with the set of values that it can take on
const properties = {
    "bedrooms": [2, 3],
    "bathrooms": [1, 2],
    "floors": [1, 2, 3, 4],
}

// Start with a single "empty" tuple
let tuples = [{}]

for (let p in properties) {
    // For each property, augment all of the old tuples
    let nextTuples = []
    for (let option of properties[p]) {
        // with each possible option
        nextTuples = nextTuples.concat(tuples.map(old => ({[p]: option, ...old})))
    }
    tuples = nextTuples;
}

with tuples ending up like

[
    {
        "floors": 1,
        "bathrooms": 1,
        "bedrooms": 2
    },
    {
        "floors": 1,
        "bathrooms": 1,
        "bedrooms": 3
    },
    ......
    {
        "floors": 4,
        "bathrooms": 2,
        "bedrooms": 3
    }
]
Sign up to request clarification or add additional context in comments.

4 Comments

I like this approach. Very simple. Let me try to implement this!
@Jon actually, almost same as your codes, it still is three nested loops.
@Sphinx ...It's not, it's only two. But more importantly, if you change the number of properties, the number of loops in the code stays the same. You only need to change the contents dictionary.
@CurtisF Thank you. This approach is wonderful and it also provides me a map to perform further future manipulations of the data :)
1

Similar to the answer from Curtis F, you can use something like

const properties = {
    "bedrooms": {min: 2, max: 3},
    "bathrooms": {min: 1, max: 2},
    "floors": {min: 1, max: 4},
}

Then build up the "tuples" in a similar manner except that your for loop needs to count from min to max for each object in properties.

1 Comment

Nice, I like this tweak to make the code more efficient :)
0

One approach you can use is a recursive function, where each layer will iterate one dimension of your matrix:

const dims = [
  { min: 2, max: 4 },
  { min: 1, max: 3 },
  { min: 1, max: 1 },
];

function matrix(dims, ...args) {
  const dim = dims[0];
  return dims.length
    ? [...new Array(dim.max - dim.min + 1)]
      .map((_,i) => i + dim.min)
      .map(x => matrix(dims.slice(1), ...args, x))
      .reduce((a, b) => [...a, ...b], [])
    : [callApi(...args)];
}

function callApi(bedrooms, bathrooms, floors) {
  return `bedrooms: ${bedrooms}, bathrooms: ${bathrooms}, floors: ${floors}`;
}

console.log(matrix(dims));

1 Comment

This assumes a particular order of the arguments in dims. This could be improved by making dims an object instead of an array.
0

Use recursive call.

Try following code:

{
    function Looping ( parameters, fn, args = [] ) {
        if ( parameters.length ) {
            let [ [ key, param ], ...pass ] = parameters;
            let promises = [];
            for ( let i = param.min; i <= param.max; i++ ) {
                promises.push( ...Looping( pass, fn, [ ...args, i ] ) );
            }
            return promises;
        }
        else {
            return [ fn( ...args ) ];
        }
    }

    const searchParams = {
        Bedrooms: { min: 2, max: 4 },
        Bathrooms: { min: 1, max: 3 },
        Floors: { min: 1, max: 1 }
    };

    function callApi ( a, b, c ) { return Promise.resolve( `Bed: ${a}, Bath: ${b}, Floor: ${c}` ); }

    console.time( 'Recursive' );
    Promise.all( Looping( Object.entries( searchParams ), ( bedrooms, bathrooms, floors ) => callApi( bedrooms, bathrooms, floors ) ) )
    .then( a => {
        console.timeEnd( 'Recursive' );
        console.log( a );
    } );
}

Recursive call type faster than mapping.

( async () => {
    await new Promise( resolve => {
        console.time( 'map' );
        function mm ( a, b ) { let r = []; for ( let i = a; i <= b; i++ ) r.push( i ); return r; }
        const properties = {
            Bedrooms: mm( 1, 100 ),
            Bathrooms: mm( 1, 100 ),
            Floors: mm( 1, 100 )
        };
        
        // Start with a single "empty" tuple
        let tuples = [{}]
        for (let p in properties) {
            // For each property, augment all of the old tuples
            let nextTuples = []
            for (let option of properties[p]) {
                // with each possible option
                nextTuples = nextTuples.concat(tuples.map(old => ({[p]: option, ...old})))
            }
            tuples = nextTuples;
        }
        let promises = [];
        function callApi ( a, b, c ) { return Promise.resolve( `Bed: ${a}, Bath: ${b}, Floor: ${c}` ); }
        for ( const i of tuples ) {
            let arg = [];
            for ( const [ k, v ] of Object.entries( i ) ) {
                arg.push( v );
            }
            promises.push( callApi( ...arg ) );
        }
        Promise.all( promises ).then( a => {
            console.timeEnd( 'map' );
            //console.log( a );
            resolve();
        } );
    } );

    await new Promise( resolve => {
        function Looping ( parameters, fn, args = [] ) {
            if ( parameters.length ) {
                let [ [ key, param ], ...pass ] = parameters;
                let promises = [];
                for ( let i = param.min; i <= param.max; i++ ) {
                    promises.push( ...Looping( pass, fn, [ ...args, i ] ) );
                }
                return promises;
            }
            else {
                return [ fn( ...args ) ];
            }
        }
    
        const searchParams = {
            Bedrooms: { min: 1, max: 100 },
            Bathrooms: { min: 1, max: 100 },
            Floors: { min: 1, max: 100 }
        };
    
        function callApi ( a, b, c ) { return Promise.resolve( `Bed: ${a}, Bath: ${b}, Floor: ${c}` ); }
    
        console.time( 'Recursive' );
        Promise.all( Looping( Object.entries( searchParams ), ( bedrooms, bathrooms, floors ) => callApi( bedrooms, bathrooms, floors ) ) )
        .then( a => {
            console.timeEnd( 'Recursive' );
            //console.log( a );
            resolve();
        } );
    } );
} )();

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.