35
\$\begingroup\$

For the past four years I've attended a private school which requires its students to wear ties. I've found that it's far easier to figure out what combination of shirt, pants, and tie that go together the night before, as it can sometimes be a tedious process. To make this process simpler, I had the idea to create a script to automatically generate new combinations of matching clothes.

As of now, I input all of my clothes into a JSON file, separated into arrays representing categories (shirts, pants, ties). The first element of each array is a another array, filled with all available colors/patterns in that category (red, blue, etc). For each color, there is an array ID'd by its color, consisting of an object describing what color/pattern pants/ties/sweaters go with that shirt color. Finally there is an array with the names of each type of clothing item that falls under that color. In case you didn't follow any of that, here's a section of my JSON file (full here):

"shirts": [{
    "colors": [
        "red"
    ],
    "red": [{
            "pants": [
                "navy",
                "khaki"
            ],
            "sweaters": [
                "navy",
                "grey"
            ],
            "ties": [
                "all"
            ]
        },
        [
            "Red Flannel",
            "That Other Red Flannel"
        ]
    ]
}],
...

It's a pretty tedious structure (IMO), but I can't think of a better implementation since there are multiple criteria that the matching clothes need to match. My method of creating a random combination is equally tedious:

$.getJSON("clothes.json", function (clothes) {
    window.clothes = clothes;
    var colors = clothes["shirts"][0]["colors"];
    $(colors).each(function (i) {
        $("#color").append("<option>" + colors[i] + "</option>");
    });
});

function randomArrayValue(array) {
    var array = eval(array);
    return array[Math.floor(Math.random() * array.length)];
}

function randomClothes(times, color, sweater, tie) {
    $("#output").html("");
    // Objects for everything
    var shirts = clothes["shirts"][0];
    pants = clothes["pants"][0];
    sweaters = clothes["sweaters"][0];
    ties = clothes["ties"][0];
    shoes = clothes["shoes"][0];
    for (var x = 0; x < times; x++) {
        if (color) {
            startShirt = shirts[color];
        } else {
            startShirt = shirts[randomArrayValue(shirts["colors"])]
        }
        shirt_optionsPants = startShirt[0]["pants"];
        shirt_optionsSweaters = startShirt[0]["sweaters"];
        shirt_optionsTies = startShirt[0]["ties"];
        // Array to hold random combinations
        combination = new Array();
        for (var i = 0; i < shirt_optionsPants.length; i++) {
            var currentPantColor = shirt_optionsPants[i];
            var pant_optionsSweaters = pants[currentPantColor][0]["sweaters"];
            if (tie) {
                var pant_optionsTies = pants[currentPantColor][0]["ties"];
            } else {
                var pant_optionsTies = new Array("all");
            }
            // Availible ties between pants and shirt, tie color has to be both present in shirt and pant or it wont be added
            var availableShirtPantTies = new Array();
            $(shirt_optionsTies).each(function (i) {
                var currentShirtTie = shirt_optionsTies[i];
                if (pant_optionsTies[0] === "all") {
                    availableShirtPantTies.push(currentShirtTie);
                } else {
                    $(pant_optionsTies).each(function (i) {
                        if (pant_optionsTies[i] === currentShirtTie) {
                            availableShirtPantTies.push(currentShirtTie);
                        } else if (currentShirtTie === "all") {
                            availableShirtPantTies.push(pant_optionsTies[i]);
                        }
                    });
                }
            });
            for (var k = 0; k < pant_optionsSweaters.length; k++) {
                var currentSweaterColor = pant_optionsSweaters[k];
                var sweater_optionsTies = sweaters[currentSweaterColor][0]["ties"];
                // Check if there are any ties that matches both the pants and shirt
                if (availableShirtPantTies.length > 0) {
                    if (sweater) {
                        var availableShirtPantSweaterTies = new Array();
                        $(sweater_optionsTies).each(function (i) {
                            var currentSweaterTie = sweater_optionsTies[i];
                            if ($.inArray(currentSweaterTie, availableShirtPantTies) >= 0) {
                                availableShirtPantSweaterTies.push(currentSweaterTie)
                            } else if (currentSweaterTie === "all") {
                                $(availableShirtPantTies).each(function (i) {
                                    availableShirtPantSweaterTies.push(availableShirtPantTies[i]);
                                });
                            } else {
                                if (sweater_optionsTies[i] === sweater_optionsTies[i]) {
                                    availableShirtPantSweaterTies.push(sweater_optionsTies[i]);
                                }
                            }
                        });
                    } else {
                        availableShirtPantSweaterTies = availableShirtPantTies;
                    }
                    $(availableShirtPantSweaterTies).each(function (i) {
                        // Assign any tie to a color
                        if (tie) {
                            if (availableShirtPantSweaterTies[i] === "all") {
                                currentTieColor = randomArrayValue(ties["colors"]);
                            } else {
                                currentTieColor = availableShirtPantSweaterTies[i];
                            }
                        } else {
                            currentTieColor = ties["colors"][0];
                        }
                        //Result object
                        var result = new Object();
                        result.shoes = randomArrayValue(pants[currentPantColor][0]["shoes"]);
                        result.pants = currentPantColor;
                        result.sweater = currentSweaterColor;
                        result.tie = currentTieColor;
                        combination.push(result);
                    });
                }
            }
        }
        var randomCombination = randomArrayValue(combination);
        pantsColor = randomCombination["pants"];
        sweaterColor = randomCombination["sweater"];
        tieColor = randomCombination["tie"];
        shoesColor = randomCombination["shoes"];
        var randomShirt = randomArrayValue(startShirt[1]);
        randomPants = randomArrayValue(pants[pantsColor][1]);
        randomSweater = randomArrayValue(sweaters[sweaterColor][1]);
        randomTie = randomArrayValue(ties[tieColor][0]);
        randomShoes = randomArrayValue(shoes[shoesColor][0]);
        //Debugging
        //console.log(randomShirt, randomPants, randomSweater, randomTie, randomShoes);
        if (sweater) {
            var sweaterContent = "Sweater: " + randomSweater + "<br />";
        } else {
            var sweaterContent = "";
        }
        if (tie) {
            var tieContent = "Tie: " + randomTie + "<br />";
        } else {
            var tieContent = "";
        }
        var content = "Shirt: " + randomShirt + "<br />Pants: " + randomPants + "<br />" + sweaterContent + tieContent + "Shoes: " + randomShoes + "<br /><br />";
        $("#output").append(content);
    }
}
$("#random").click(function () {
    randomClothes(1, "", true, true);
});

This isn't like anything I've really coded before, and I'm not the best when it comes to visualizing randomness. My original method of coding this did not take into account that certain shirts could go with certain ties, but those shirts might go with sweaters that the ties or pants do not. Consequently, the code is rather large. I have to believe, however, that there is a much better solution to what I'm trying to achieve. Also, I would like to add in implementations for regular T-Shirts or anything for that matter, but as it stands now, the structure is much too strict to allow for much modification.

Oh yes, if it wasn't clear, the code hinges off of the shirt. So it picks a random shirt, then sees what matches it. If the code wasn't as strict, it would be nice to be able to start with any article of clothing, such as pants.

\$\endgroup\$
  • \$\begingroup\$ Could you post a jsfiddle demo to test? \$\endgroup\$ – elclanrs Feb 8 '14 at 23:09
  • \$\begingroup\$ @elclanrs Sure thing! \$\endgroup\$ – Charlie Feb 9 '14 at 0:34
22
+150
\$\begingroup\$

Nice idea. I really like what you've done, but some of your implementation can definitely be improved. Let's start with the JSON.

  • Some of those arrays are not needed. You wrap each clothes item in [], there's no need for this as there's just one item in the array! In fact you should only be using arrays to group like items. I'm also going to get rid of your colour array since it seems redundant (more on this later). Consider this redesign;

    "shirts": {
        "red": {
           "items": [ 
             "Red Flannel",
             "That Other Red Flannel"
           ],
           "matches": {
             "pants": [
               "navy",
               "khaki"
             ],
             "sweaters": [
               "navy",
               "grey"
             ],
             "ties": [
               "all"
             ]
           }
        }
    }
    

    Do you see how that instantly makes it easier for us to look items up and iterate over matches?

    Now you can remove all the [0] accessors:

    Now you can select colours with: shirts[color], and iterate over matches with;

     for (var match in shirts[color].matches) { 
    
     }
    

    Which brings me to my next point.

  • You seem to be using bracket notation a lot to access the items in your object. When the accessor isn't a variable then you can use dot notation.

    shirts[color]['matches'] === shirts[color].matches
    

    The distinction helps us to distinguish how we're accessing the property at a glance.

  • The function randomArrayValue is using an unnecessary eval. Eval is not needed most of the time, and is often misused by new JavaScript devs. You could just do;

    function randomArrayValue(arr) {
      return arr[Math.floor(Math.random() * arr.length)];
    }
    

    As an aside you can also use ~~ instead of Math.floor, and it's slightly faster.

  • You're missing a lot of var statements. This is generally bad as variables declared without var are set as globals, accessible from anywhere. There's nothing as annoying as global variable bugs. You can get notiofied of issues like this by including the string 'use strict'; at the top of the program. This tells the browser to be stricter with compilation.

  • Consider literals instead of new Objects. For example:

    combination = new Array(); // okay
    combination = [];          // better
    
    result = new Object(); // not great
    result = {};           // awesome
    
  • For your result object, you'll be much better off creating an Object literal;

    var result = {
      shoes: randomArrayValue(pants[currentPantColor].shoes),
      pants: currentPantColor,
      sweater: currentSweaterColor,
      tie: currentTieColor
    };
    combination.push(result);
    
  • Back to that colour object... You can get the keys of an Object using Object.keys(obj). So if you want to know all the colours that your shirts come in you can call Object.keys(clothes.shirts). If you want to iterate over all the colours then you can for...in:

    for (var colour in clothes.shirts) {
      var items = clothes.shirts[colour].items,
        matches = clothes.shirts[colour].matches;
    }
    

That's enough to start with, let me know if you'd like me to point out how you can take advantage of some of these designs within your solution.

\$\endgroup\$
  • 1
    \$\begingroup\$ I'm going to read this in depth, but 2 quick things: 1.) I wrote this code a few months ago and after rediscovering it yesterday, I saw I used eval and I had absolutely no idea why. I think it was left over from an earlier version 2.) I also only recently started using dot notation \$\endgroup\$ – Charlie Feb 9 '14 at 15:15
  • 1
    \$\begingroup\$ My main concern with my code is about the addition of other clothing items. Do you see any solution where it's easier to add in more functionality? I see it as different functions, each being able to sort from the list, and then all I have to do is pipe in the data and it gives me corresponding items. \$\endgroup\$ – Charlie Feb 9 '14 at 16:23
  • \$\begingroup\$ I ended up making the changes, and it looks a bit cleaner. The logic, however, still seems redundant in places. \$\endgroup\$ – Charlie Feb 9 '14 at 20:29
  • \$\begingroup\$ @Jivings Any word on improving the logic? \$\endgroup\$ – Charlie Jul 11 '14 at 5:10
6
\$\begingroup\$

To accomplish what you're asking for you need to switch your approach to a functional approach and significantly rewrite your code. What you're looking for is best solved by the concept of folding.

Check it!

You call a function that finds a match for the shirt you picked. You say "hey, here's a shirt, find me some matching pants!" It then returns not just some matching pants but an object containing both the shirt and pants it just found for you, and here's why--you then turn around and pass that object into the same function, saying "yo, here's what I've picked out so far, now find me a matching sweater!" Your function adds a random match to what you have so far and returns your whole outfit so far, which is then passed in to the next function.

Check out the array reduce method. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

If you use this you could create an array that holds a list of items you want to retrieve, and then call reduce on it, e.g. ["shirt", "tie", "sweater"].reduce(findMatches); where findMatches is your callback function.

This also has the added bonus of keeping all of your logic in one place. And, as long as your data is updated correctly, adding new articles of clothing is as simple as adding a string to the array!

\$\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.