Sometimes it can be handy to find the day of the year* that a particular date falls on in JavaScript.
Let’s say you want to get the day of the year for Pie Day (14 Mar 2024). Check out the code:
*This tutorial is for the Gregorian Calendar.
Google Workspace and Software Development
Sometimes it can be handy to find the day of the year* that a particular date falls on in JavaScript.
Let’s say you want to get the day of the year for Pie Day (14 Mar 2024). Check out the code:
*This tutorial is for the Gregorian Calendar.
Sometimes we want to compare the difference between two arrays in JavaScript and return the remaining values in a new array.
This can be useful for inventory or purchase management where we could compare the selected items against the available stock, for example.
In this tutorial, we will cover two approaches to this:
Let’s get cracking!
Continue reading “Get the Difference Between Two Arrays in JavaScript”
I recently came across (yet another) situation where I needed to work with a text-based ranking system. You know, when some gem decided to create a ranking system like good, great, awesome, spectacular and then we need to store each user’s highest level of achievement over a period.
This occurs with surprising regularity in my work, most often when needing to work with spreadsheet data that a user has created. We can’t really change the data on the spreadsheet so we need to handle this with some code instead.
Not ideal, I know. We could complain about it ad nauseam or simply live with it and deal with it using some JavaScript magic.
In this tutorial, we will cover two approaches to this. Both approaches have their own uses. We will go over a few use cases so you get an idea of when these sorts of things come up.
Let’s get cracking.
Note: Sometimes it takes a couple of examples to figure out a concept. Other times, you can figure it out in an instant. Feel free to read as much as you need to understand the concept.
Table of Contents
Let’s say I am having a poker night with some friends and want to only record my best hand for the night, you know, to brag later.
First, we should list all the poker hands from best to worst:
As the night starts, my first hand was a ‘three of a kind’. So I make a note. Then, I only get a ‘pair’ for my next hand. I’m not writing that down. It’s worse. Next, I get a ‘flush’. Awesome, a ‘flush’ is better than a ‘three of a kind’, so I will store that one and put a line through my previous record.
Do you get the picture?
Cool. Let’s write some code.
We will need to store our list of poker hands in order and also create a variable to store the best hand we get during the game. We have two choices here. I will go through both and then discuss performance.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
const poker = [ "High Card", "Pair", "Two pair", "Three of a kind", "Straight", "Flush", "Full house", "Four of a kind", "Straight flush", "Royal flush", ]; let bestHand = "Three of a kind"; /** * Compares the current text item agains the new item and returns the most * important one. * @param {Array} list * @param {String} storedItem * @param {String} newItem * @returns most important value */ function setMostImportant_Array(list, storedItem, newItem) { return list.indexOf(storedItem) >= list.indexOf(newItem) ? storedItem : newItem; }; // The best hand we get get for the night. let bestHand = "Three of a kind"; console.log("First hand of the night: ", bestHand); // Three of a kind bestHand = setMostImportant_Array(poker, bestHand, "Pair"); console.log("Best hand is: ", bestHand); // Three of a kind bestHand = setMostImportant_Array(poker, bestHand, "Flush"); console.log("Best hand is: ", bestHand); // Flush bestHand = setMostImportant_Array(poker, bestHand, "Straight"); console.log("Best hand is: ", bestHand); // Flush bestHand = setMostImportant_Array(poker, bestHand, "Three of a kind"); console.log("Best hand is: ", bestHand); // Flush bestHand = setMostImportant_Array(poker, bestHand, "Straight flush"); console.log("Best hand is: ", bestHand); // Straight flush |
So what’s going on?
First, we store our list of all possible poker hands in an array called ‘poker’. Starting with the lowest hand on the left and finishing with the best hand on the right (bottom of the array). Lines 1-12
Arrays in JavaScript maintain their order so we know that “High Card” will always be in position poker[0]
and “Royal Flush” will be in position poker[9]
. We will be using the array’s order in a minute to check if we should be storing our next hand or not.
Next, we need a place to store our highest hand for the night. We set the variable bestHand
for this, using JavaScript ‘let’ variable declaration to allow us to change the variable as we update it with a better hand. Line 14
We now create the function setMostImportant_Array()
. This function takes 3 arguments:
bestHand
.The function returns the most important item. This is done with a simple ternary operator, which is a kind of simplified ‘if’ statement.
On line 5, we compare the position of our stored bestHand
value in its position in the poker array list against the new hand. We do this by using the indexOf method which returns the first occurrence of the matching item in an array as an index of that array. If the current bestHand
is greater than or equal to the new hand then we return the current bestHand
. Otherwise, we return the new hand.
After we have set up our function we can now run it. Our second hand of the night was a ‘pair’ so we updated our best hand to our function adding in, poker as our array list, bestHand
as our stored item, and “Pair” as our new item.
bestHand = setMostImportant_Array(poker, bestHand, "Pair");
Because a pair is not as important as a ‘three of a kind’ we kept the current value.
On the next hand, we got a ‘Flush’ which is more valuable than a ‘Three of a kind’ so using our function again we see that bestHand
now stores the ‘Flush’ as the most important value.
We can achieve the same outcome by storing data as an object. Let’s take a look at the code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
const poker = { "Royal flush": 10, "Straight flush": 9, "Four of a kind": 8, "Full house": 7, "Flush": 6, "Straight": 5, "Three of a kind": 4, "Two pair": 3, "Pair": 2, "High Card": 1, }; /** * Compares the current text item agains the new item and returns the most * important one. * @param {Object} obj - the name ranked item: position as a number. * @param {String} storedItem * @param {String} newItem * @returns most important value */ function setMostImportant_Object(obj, storedItem, newItem) { return obj[storedItem] > obj[newItem] ? storedItem : newItem; } // The best hand we get get for the night. let bestHand = "Three of a kind"; console.log("First hand of the night: ", bestHand); // Three of a kind bestHand = setMostImportant_Object(poker, bestHand, "Pair"); console.log("Best hand is: ", bestHand); // Three of a kind bestHand = setMostImportant_Object(poker, bestHand, "Flush"); console.log("Best hand is: ", bestHand); // Flush bestHand = setMostImportant_Object(poker, bestHand, "Straight"); console.log("Best hand is: ", bestHand); // Flush bestHand = setMostImportant_Object(poker, bestHand, "Three of a kind"); console.log("Best hand is: ", bestHand); // Flush bestHand = setMostImportant_Object(poker, bestHand, "Straight flush"); console.log("Best hand is: ", bestHand); // Straight flush |
In our poker
object, we store the names of each hand as a property string and then each property’s value is its position or order of importance in the group of data. Lines 1-12
Our setMostImportant_Object()
function is a lot simpler than the array function above. Here we will use our ternary operator, but this time we simply are checking to see if the bestHand
property value stored in our poker
object is greater than the new hand property value. Line 14-24
Our function takes on arguments in the same way as the array function and will store the new property in the bestHand
variable if the hand is better than the one stored.
The most predictable and irritating answer first. It depends.
Running a benchmark test on the two they are fairly comparable, but over larger sets of stored ranked text, the object approach would perform better.
However, my preference would slant to the array approach if my ranking is not static. This is probably a rare case, but imagine if your text-based ranking system changes regularly and you need to update the order of you ranking system.
Moving your data around an array and cutting it out of one position and putting it in another is fairly simple with an array. With an object, however, we need to update the current value of the property that is going to change and then move all the other properties around than need to change. This would definitely be more memory intensive and harder to code.
Here is an example.
Imagine that we are investing in a fictional commodity market. As supply and demand fluctuate, a commodities value may move to become greater or lesser than other commodities in the same market.
Our goal in this scenario is to keep the best value commodity available to us. If the commodity’s value position compared to other commodities shifts, then we need to ensure that we always keep the best commodity available to us.
Let’s use this fictional list of commodities as our example:
We are first presented with the opportunity to buy ocoumbre. Having no investments in the market, we make the purchase.
Our next opportunity is to purchase tryzatium. Currently, this item is less valuable than ocoumbre so we won’t buy it.
Enzome comes up next. Being of higher value, we purchase it and sell off our ocoumbre.
Then the next day the market changes. A new large deposit of enzome is found and floods the market, reducing its value to below tryzatium. So when we get an offer to buy malacite we jump at it.
Let’s write some simple code to simulate this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
/** * Compares the current text item agains the new item and returns the most * important one. * @param {Array} list * @param {String} storedItem * @param {String} newItem * @returns most important value */ function setMostImportant_Array(list, storedItem, newItem) { return list.indexOf(storedItem) >= list.indexOf(newItem) ? storedItem : newItem; } /** * Move a seleted item to a new position in the order. * @param {Array} list - Array of ordered items. * @param {String} item - The item to change position. * @param {Number} newIdx - The new index of the item. */ function moveItem(list, item, newIdx) { const currentIdx = list.indexOf(item); list.splice(newIdx, 0, list.splice(currentIdx, 1)[0]); } let commoditiyByVal = [ "tryzatium", // Lowest in value "ocoumbre", "malcatite", "enzome", "parl", "obvoster", // Highest in value ]; console.log("Current market order: ", commoditiyByVal) // Initial Value let commodityHeld = "ocoumbre"; console.log("First commodity purchase: ", commodityHeld); // Commodity offered. commodityHeld = setMostImportant_Array( commoditiyByVal, commodityHeld, "tryzatium" ); console.log("Commodity held: ", commodityHeld); // ocoumbre // Commodity offered. commodityHeld = setMostImportant_Array( commoditiyByVal, commodityHeld, "enzome" ); console.log("Commodity held: ", commodityHeld); // enzome // Commodity offered. commodityHeld = setMostImportant_Array( commoditiyByVal, commodityHeld, "enzome" ); console.log("Commodity held: ", commodityHeld); // enzome // #### MARKET CHANGE #### moveItem(commoditiyByVal, "enzome", 1); console.log("New market order: ", commoditiyByVal); // Commodity offered. commodityHeld = setMostImportant_Array( commoditiyByVal, commodityHeld, "malcatite" ); console.log("Commodity held: ", commodityHeld); // malcatite // Commodity offered. commodityHeld = setMostImportant_Array(commoditiyByVal, commodityHeld, "parl"); console.log("Commodity held: ", commodityHeld); // parl // #### MARKET CHANGE #### moveItem(commoditiyByVal, "tryzatium", 4); console.log("New market order: ", commoditiyByVal); // Commodity offered. commodityHeld = setMostImportant_Array( commoditiyByVal, commodityHeld, "enzome" ); console.log("Commodity held: ", commodityHeld); // parl // Commodity offered. commodityHeld = setMostImportant_Array( commoditiyByVal, commodityHeld, "tryzatium" ); console.log("Commodity held: ", commodityHeld); // tryzatium |
The results of which should appear like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
First commodity purchase: ocoumbre Commodity held: ocoumbre Commodity held: enzome Commodity held: enzome New market order: (5) ['tryzatium', 'enzome', 'malcatite', 'parl', 'obvoster'] Commodity held: malcatite Commodity held: parl New market order: (5) ['enzome', 'malcatite', 'parl', 'obvoster', 'tryzatium'] Commodity held: parl Commodity held: tryzatium |
Just like in the first example, when a better commodity is presented to us we replace it with our current commodity. When there is a change to the order of the market, then we also see a change in how we prioritise what we keep.
1 2 3 4 5 6 7 8 9 10 |
/** * Move a seleted item to a new position in the order. * @param {Array} list - Array of ordered items. * @param {String} item - The item to change position. * @param {Number} newIdx - The new index of the item. */ function moveItem(list, item, newIdx) { const currentIdx = list.indexOf(item); list.splice(newIdx, 0, list.splice(currentIdx, 1)[0]); } |
The moveItem()
function allows us to change the order of priority of our list of commodities.
The function takes 3 arguments as parameters:
commoditiyByVal
array.In the first task of the function, we need to get the current index of the item. We do this by finding its index in the array with the indexOf
method. Line 8
Next, we use the JavaScript splice method to cut the item out of the array (The inner splice) and then add it back into the array at the desired location (The outer splice). Line 9
The outer splice takes 3 arguments:
splice(start index, deleteCount, item to add)
So the thing that actually compelled me to write about this was a part of a Google Apps Script project – Google Apps Script is built on Google’s V8 engine for EcmaScript JavaScript.
I was fetching requests from an external API that requested that an event be triggered at a particular interval. We use Clock Triggers in Google Apps Script but you might know them as Cron Jobs.
The API contained a list of text-based triggers that looked a little like this:
Anyway, we have limitations and quotas to how many clock triggers we have running in one account at one time so I always try and limit these. So if I can set a daily trigger for a user I will prefer it over a 1-minute trigger so that they don’t get any quota errors or issues.
In my example, when a user requires a trigger to be set for a project at a certain interval, the code will check against the current setting of all existing triggers for the project and update the trigger accordingly.
So for example, if a user sets their first project item to run every six hours, then the trigger will only run every six hours. If they add another item that needs to be run daily then that item will be initiated every fourth time the trigger is run ( 4 x 6 = 24 hours).
However, if they create a third item that requires it to be run every 30 minutes then we need the change the frequency that the trigger is called to 30-minute intervals. This means that the first item needs to be invoked every 12th occasion the 30-minute trigger is run (6 x 2 = 12). Likewise, the daily trigger needs to be invoked every 48th time the trigger is run (24 hrs x 2).
Let’s see how this looks in the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/** * Compares the current text item agains the new item and returns the most * important one. * @param {Object} obj - the name ranked item: position as a number. * @param {String} storedItem * @param {String} newItem * @returns most important value */ function setMostImportant_Object(obj, storedItem, newItem) { return obj[storedItem] > obj[newItem] ? storedItem : newItem; } const triggerIntervals = { "1 minute": 5, "30 minutes": 4, "1 hour": 3, "6 hours": 2, daily: 1, }; let interval = "6 hours"; console.log("Initial Trigger: ", interval); interval = setMostImportant_Object(triggerIntervals, interval, "daily"); console.log("Current interval: ", interval); // 6 hours interval = setMostImportant_Object(triggerIntervals, interval, "30 minutes"); console.log("Current interval: ", interval); // 30 minutes /** * RESULTS * Initial Trigger: 6 hours * Current interval: 6 hours * Current interval: 30 minutes */ |
I’d love to hear if you have some examples of when you might need to handle text-based ordering systems and what approach you took. Feel free to add them in the comments below.
If you have found the tutorial helpful, why not shout me a coffee ☕? I'd really appreciate it.
Need help with Google Workspace development?
Go something to solve bigger than Chat GPT?
I can help you with all of your Google Workspace development needs, from custom app development to integrations and security. I have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.
Schedule a free consultation today to discuss your needs and get started or learn more about our services here.
~Yagi
Recently, I needed a way to ensure that my JavaScript array of objects does not contain any duplicate objects based on an ‘id’ key. While I do enjoy more common approaches to this solution with the use of a simple ‘for’ loop it is always a bit of fun to see how folks have come up with modern solutions.
It wasn’t too long before I stumbled across this ES6 one-liner:
1 |
let uniqueObjArray = [...new Map(objArray.map((item) => [item["id"], item])).values()]; |
Pretty neat, huh?
Here are two locations I found them:
But as many code oneliners are, they are a little tricky to understand. So I wanted to spend some time understanding how this worked and thought, you might find this a little interesting too.
Let’s take a look at an example and then work through each bit as we go.
Table of Contents
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
const myObjArray = [ { name: "Eva Devore", character: "Evandra", episodes: 15, }, { name: "Alessia Medina", character: "Nixie", episodes: 15, }, { name: "Kendall Drury", character: "DM", episodes: 15, }, { name: "Thomas Taufan", character: "Antrius", episodes: 14, }, { name: "Alessia Medina", character: "Nixie", episodes: 15, }, ]; // Creates an array of objects with unique "name" property values. let uniqueObjArray = [ ...new Map(myObjArray.map((item) => [item["name"], item])).values(), ]; console.log("uniqueObjArray", uniqueObjArray); // LOGS: // uniqueObjArray [ // { name: 'Eva Devore', character: 'Evandra', episodes: 15 }, // { name: 'Alessia Medina', character: 'Nixie', episodes: 15 }, // { name: 'Kendall Drury', character: 'DM', episodes: 15 }, // { name: 'Thomas Taufan', character: 'Antrius', episodes: 14 } // ] |
In the example above, we want to create a unique array of objects based on the ‘name’ property of each object set.
You can see that there is a duplicate property in positions 1 and 4 with key ‘name’ and value, ‘Alessia Medina’ that we need to remove.
You can also change the key to either the ‘character’ or ‘episodes’ property.
When the distinct array of objects is created it will set the last duplicate object as the object value. Why? Because the script will essentially reassign the ‘name’ property each time it loops through the array when creating the new map.
Let’s start breaking down the script so we can see how each one operates.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Creates a new 2d array with the selected key in position 0 and the object in position 1 of each iteration. let test_uniqueObjArray_map = myObjArray.map((item) => { return [item["name"], item]; }); // LOGS: test_uniqueObjArray_map // [ // ["Eva Devore", { name: "Eva Devore", character: "Evandra", episodes: 15 }], // ["Alessia Medina",{ name: "Alessia Medina", character: "Nixie", episodes: 15 }], // ["Kendall Drury", { name: "Kendall Drury", character: "DM", episodes: 15 }], // ["Thomas Taufan", { name: "Thomas Taufan", character: "Antrius", episodes: 14 }], // ["Alessia Medina",{ name: "Alessia Medina", character: "Nixie", episodes: 15 }] // ]; |
The first task of this script remaps the array of objects using the JavaScript map() method. This method takes a function, which in our is an arrow function.
Map method arrow functions generally look like this:
1 |
myArray.map((element) => <em>return new element as a result of a callback</em>) |
As an ordinary function, it would look like this:
1 2 3 |
myArray.map(function(element){ return <em>new element as a result of a callback</em> }) |
In our example above, we have our callback arguments on a new line so we will also need to include curly braces {}
.
With the map method, the function will act on each array and return the result to generate a new array of the same length.
For us, our call back condition rebuilds each array to make a sub-array containing the value of each name key in the array as the zeroeth element and the object at the first element.
1 2 3 4 5 |
... [item["name"], item]; ... |
So the first element in the new array will look like this:
1 2 3 4 5 |
... ["Eva Devore", { name: "Eva Devore", character: "Evandra", episodes: 15 }] ... |
Before we continue, let’s take a quick look at a basic Map process on a 2d array:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Creates a key-value map of a 2d array let valuesObject = new Map([ ["key_one", "val_one"], ["key_two", "val_two"], ["key_three", "val_three"], ]); console.log("valuesObject", valuesObject); // LOGS: valuesObject Map { // 'key_one' => 'val_one', // 'key_two' => 'val_two', // 'key_three' => 'val_three' // } |
To be frank, I didn’t really understand the Map object too well until I explored this script.
Map object stores key-value pairs similar to an Object. However, the Map maintains the insertion order of the properties. You’ll see Map objects often displayed like this when logged out in the console.
1 |
'key' => 'value' |
Map can be iterated through in a similar way to a 2d array with the zeroeth element as a key and the next element as a value for each property of the map – ['key', 'value']
.
Alternatively, we can also generate a Map from a 2d array as we did in the example above – turning each sub-array into a key-value pair.
Back to our main example…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// PREVIOUS LOG: test_uniqueObjArray_map // [ // ["Eva Devore", { name: "Eva Devore", character: "Evandra", episodes: 15 }], // ["Alessia Medina",{ name: "Alessia Medina", character: "Nixie", episodes: 15 }], // ["Kendall Drury", { name: "Kendall Drury", character: "DM", episodes: 15 }], // ["Thomas Taufan", { name: "Thomas Taufan", character: "Antrius", episodes: 14 }], // ["Alessia Medina",{ name: "Alessia Medina", character: "Nixie", episodes: 15 }] // ]; // Create a new Map from our mapped data. let test_uniqueObjArray_NewMap = new Map(test_uniqueObjArray_map); // LOGS: test_uniqueObjArray_NewMap Map { // 'Eva Devore' => { name: 'Eva Devore', character: 'Evandra', episodes: 15 }, // 'Alessia Medina' => { name: 'Alessia Medina', character: 'Nixie', episodes: 15 }, // 'Kendall Drury' => { name: 'Kendall Drury', character: 'DM', episodes: 15 }, // 'Thomas Taufan' => { name: 'Thomas Taufan', character: 'Antrius', episodes: 14 } // } |
We are using the data we retrieved from our previous example here to remove some of the clutter from the process. I have added those results at the top of the code block above.
In this example, we simply apply new Map
to this array of data. By doing this Map turns into a type of Object with a key-value pair. Now keep in mind that Object keys are the highlander of data types – there can be only one.
What does this mean beyond a bad joke that really shows my age?
It means that each key must be unique. All of our keys are now the names of our users. The new Map constructor process will then iterate through each name and store it and then assign its value. If a key already exists it will overwrite it with this next value with the same key name.
This means that the last duplicate key will always be displayed. Effectively only storing unique values.
We can generate iterators to go through each key or value with the keys()
and values()
methods respectively.
We will have a look at the keys() method first quickly.
Let’s apply keys()
to our test_uniqueObjArray_NewMap Map
we generated above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// PREVIOUS LOG: test_uniqueObjArray_NewMap Map { // 'Eva Devore' => { name: 'Eva Devore', character: 'Evandra', episodes: 15 }, // 'Alessia Medina' => { name: 'Alessia Medina', character: 'Nixie', episodes: 15 }, // 'Kendall Drury' => { name: 'Kendall Drury', character: 'DM', episodes: 15 }, // 'Thomas Taufan' => { name: 'Thomas Taufan', character: 'Antrius', episodes: 14 } // } console.log("test_uniqueObjArray_NewMap_keys", test_uniqueObjArray_NewMap_keys); // LOGS: test_uniqueObjArray_NewMap_keys [Map Iterator] { // 'Eva Devore', // 'Alessia Medina', // 'Kendall Drury', // 'Thomas Taufan' // } |
As you can see this produces an iterator of all the (unique) keys in our data as a Map Iterator. It’s not quite an array of objects, but it allows us to iterate over each key to do something with it.
The same is true for the values()
method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// PREVIOUS LOG: test_uniqueObjArray_NewMap Map { // 'Eva Devore' => { name: 'Eva Devore', character: 'Evandra', episodes: 15 }, // 'Alessia Medina' => { name: 'Alessia Medina', character: 'Nixie', episodes: 15 }, // 'Kendall Drury' => { name: 'Kendall Drury', character: 'DM', episodes: 15 }, // 'Thomas Taufan' => { name: 'Thomas Taufan', character: 'Antrius', episodes: 14 } // } let test_uniqueObjArray_NewMap_values = test_uniqueObjArray_NewMap.values(); console.log( "test_uniqueObjArray_NewMap_values", test_uniqueObjArray_NewMap_values ); // LOGS: test_uniqueObjArray_NewMap_values [Map Iterator] { // { name: 'Eva Devore', character: 'Evandra', episodes: 15 }, // { name: 'Alessia Medina', character: 'Nixie', episodes: 15 }, // { name: 'Kendall Drury', character: 'DM', episodes: 15 }, // { name: 'Thomas Taufan', character: 'Antrius', episodes: 14 } // } |
Here we want to get an iterator of our values so that we can recreate an array of objects again.
Using the values() iterator method we now have our Map values ready to go.
Now that we have an iterator of our unique values we can now place them in our spread syntax – “...
“.
When you apply the spread syntax on an array, it will add each item of an iterable to the array. Take a look at what it does to our Map values.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// PREVIOUS LOG: test_uniqueObjArray_NewMap_values [Map Iterator] { // { name: 'Eva Devore', character: 'Evandra', episodes: 15 }, // { name: 'Alessia Medina', character: 'Nixie', episodes: 15 }, // { name: 'Kendall Drury', character: 'DM', episodes: 15 }, // { name: 'Thomas Taufan', character: 'Antrius', episodes: 14 } // } let test_uniqueObjArray_NewMap_values_asArray = [ ...test_uniqueObjArray_NewMap_values, ]; // LOGS: test_uniqueObjArray_NewMap_values_asArray [ // { name: 'Eva Devore', character: 'Evandra', episodes: 15 }, // { name: 'Alessia Medina', character: 'Nixie', episodes: 15 }, // { name: 'Kendall Drury', character: 'DM', episodes: 15 }, // { name: 'Thomas Taufan', character: 'Antrius', episodes: 14 } // ] |
This is similar to using the Array.from()
static method that would look like this:
1 2 3 |
let test_uniqueObjArray_NewMap_values_asArray = Array.from( test_uniqueObjArray_NewMap_values ); |
So how does this one-liner stack up against a more traditional for-loop like this?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
var array = [ { name: "Eva Devore", character: "Evandra", episodes: 15, }, { name: "Alessia Medina", character: "Nixie", episodes: 15, }, { name: "Kendall Drury", character: "DM", episodes: 15, }, { name: "Thomas Taufan", character: "Antrius", episodes: 14, }, { name: "Alessia Medina", character: "Nixie", episodes: 15, }, ];; var unique = []; var distinct = []; for( let i = 0; i < array.length; i++ ){ if( !unique[array[i].name]){ distinct.push(array[i].name); unique[array[i].name] = 1; } } console.log(distinct) |
Surprisingly better than I thought it would, to be honest.
Running a benchmark test with jsbench.me, the one-liner ran only 13.74% slower. Which is pretty good compared to some of the other options I found out there.
So should you be using this oneliner over the for
loop? Is an impressive one-liner better than something more clear? To be honest, I am on the fence.
I do like the way this script operates. It is clean and once I got my head around the Map object, it did make a lot of sense. I think if I saw something like this in the wild I could pretty easily identify what it was for and see that it was a nice short solution to a problem.
I don’t think I would use this approach when I need to iterate over objects in the many thousands. Then speed becomes important. But if I need something in my toolkit to solve a problem like this, then I am definitely going to use it.
I have an example of how I used the code to check for duplicate selections of files in Google Drive here:
https://yagisanatode.com/2021/06/27/creates-a-google-workspace-add-on-file-picker-card-with-cardservice-that-opens-a-google-picker-in-an-overlay-window-google-apps-script/
What do you think? Is it too abstract or is it elegant?
Did this tutorial help you understand the JavaScript one-liner better? Do you think you would apply it in your own projects?
Please let me know your thoughts in the comments below. I really enjoy hearing how things are used in the wild.
I am currently working on a larger project at the moment that requires a lot of front-end wrangling. As a part of this project, I needed to create Button Items that are generated by the user from both an HTML select element for one section and, a comma-separated text input in another section.
When a user selects an item from a select menu, a button appears in a desired area with the name of the selection and a small “X” that can be clicked to remove the item. Likewise, if a user wishes to create a bunch of items separated by commas in an input element and either hit the “Enter” or the “Add” button, then those items will be transformed as a bunch of individual buttons that the user can remove and change.
The buttons essentially become the user’s selection of items.
I also needed to be able to get a list of those item buttons for when I submit a form server-side or for some other task.
As a result, I created a small library called itemButton().
Table of Contents
Note: If you have a bit of experience with front-end, then all you may need is to grab the CSS code and input.js file. If you need some further explanation you can find the information below.
The itemButton()
library:
Take a look at an example:
My inability to consistently fail to place an ‘e’ at the end of giraffe notwithstanding, you can see that the user can select items from a select element and they will be displayed in a chosen area below. Further, when the user types some words and separates them by a comma they are displayed as buttons in a div below it.
You might have also noted that the select element will only display two-item buttons, while the comma-separated list will display up to 10. You are able to create a maximum limit of any of your text input or select elements you are running itemButton()
on.
When the user submits their items, itemButton()
will:
As you can see in the video above, when the user clicks the Log Items button, an object of ids and values for each item is listed in the browser console using the list()
method in itemButton()
. This is most useful for when you are submitting data to the server.
Let’s have a gander at the code:
I have chosen to put the Javascript itemButton()
library in an input.js file. In my project, I have other small classes or libraries in that file too. It is up to you how you want to add the code.
The CSS for the buttons is separate, and I have added it to my universal style.css file. Again you can put it anywhere you think works for you.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
export { itemButton }; /** * Add, remove and list buttons from a coma separated text input or * from a select box. */ function itemButton() { let publicAPI = {}; /** * Adds a deletable button from either an input or select tag. * @param {string} items - single item or string of comma separated items. * @param {string} selectionsLoc - id of the location the item buttons will be placed. * @param {number} numSelections - Total number of items permitted. */ publicAPI.add = (items, selectionsLoc, numSelections) => { //Get the current list of button items. let currentItems = publicAPI.list(selectionsLoc); // If no items exists in button location item counter equals numSelection // else subtract the length of he current items. let itemCounter = !currentItems ? numSelections : numSelections - currentItems.length; if (itemCounter < 1) return; // If zero then max number of items has been met. // ****Validate***** // separate by coma (if it exists) then filter our blank items. items .split(",") .filter((item) => item.trim().length != 0) .forEach((item) => { if (itemCounter < 1) return; //Reduce string length to 30 characters. let itemShort = item.substring(0, 25); // ****Create ID and Values***** // Create ID let itemID = itemShort .trim() .replace(/\s+/g, "-") //Replace spaces with dashes. .replace(/[^0-9a-z-]/gi, "") //Remove anything that isn't alphanumeric. .toLowerCase(); //If itemID starts with a number add teh letter "a" so it is a valid HTML ID. itemID = isNaN(itemID[0]) ? itemID : `a${itemID}`; //Create Item Value let itemVal = itemShort.replace(/[^0-9a-z ]/gi, ""); //Remove anything that isn't alphanumeric. //Check if ID exits in current lists. let alreadyExists = false; if (currentItems) { for (let i = 0; i < currentItems.length; i++) { if (currentItems[i].id === itemID) { return (alreadyExists = false); } } } if (alreadyExists) return; // ****Create button***** //Create new span let selectionSpan = document.createElement("span"); selectionSpan.setAttribute("value", itemVal); selectionSpan.setAttribute("id", itemID); selectionSpan.setAttribute("class", "item-btn"); selectionSpan.innerHTML = `${itemVal} <button type="button" id="${itemID}" class="item-x-btn">✖</button>`; let selectLoc = document.querySelector(`#${selectionsLoc}`); selectLoc.appendChild(selectionSpan); // ****Add event listener for new button attached to item.**** let selectedSpan = selectLoc.querySelector(`#${itemID}`); //Add an event listener on the "x" button to remove the item if clicked. selectedSpan.querySelector("button").addEventListener("click", () => { selectedSpan.remove(); }); --itemCounter; currentItems = publicAPI.list(selectionsLoc); }); }; /** * Get all the items in a selected tag location or false if none exist. * @param {string} selectionsLoc id of the location the item buttons will be placed. * @return {object|boolean} object of all current item buttons in selected tag * containing {id: , val: }. If none then false. */ publicAPI.list = (selectionsLoc) => { let items = document.getElementById(selectionsLoc).querySelectorAll("span"); //If no items exists return false if (items.length == 0) { return false; } return Object.keys(items).map((item) => { return { id: items[item].id, val: (() => { let val = items[item].innerText; return val.substr(0, val.length - 2); })(), }; }); }; return publicAPI; } |
Add this to your main CSS file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/* Buttons style for item selections such as categories and tags. */ .item-btn { background-color: #cfcdcd; color: rgb(24, 24, 24); border-radius: 5px; border: none; padding: 2px; text-align: center; display: inline-block; margin: 2px; } .item-x-btn { display: inline-block; border: none; background-color: #cfcdcd; box-shadow: none; margin-left: 1px; border-radius: 50%; color: rgb(24, 24, 24); font-size: small; line-height: 1.28; transition: 0.3s; } .item-x-btn:hover { background-color: firebrick; color: whitesmoke; cursor: pointer; } .item-x-btn:focus { outline: none; } |
First, you will need to create your select or text input elements. Make sure that they have an appropriate ID that we can reference later. You can have as many select or text input elements as you want to reference the itemButton()
class.
Take a look at the example below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Example</title> <link rel="stylesheet" type="text/css" href="main.css" media="screen" /> </head> <body> <div class="container"> <h1 align="center">Create removable button item lists from text input and select tags</h1> <!-- Selection menu used to create item buttons --> <div> <div> <label for="items">Add your selection</label> <select id="items" name="items"></select> </div> </div> <!-- Comma separated list that creates item buttons. --> <div> <div> <label for="tags" >Add your tags <em>(separate by comma): </em></label ></label> <input type="text" name="tags" id="tags" /> </div> <div> <button id="addTag" type="button">Add</button> </div> </div> </form> </div> |
Here we have added a select element with an id of “items”. We also have a text input with an ID of “tags”. The “tags” element also has an “addTag” button that the user can use to add their tags.
Next, we need to create a location to display your item buttons. In the example below, I have used a div directly below the select or input elements.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<!-- Selection menu used to create item buttons --> <div> <div> <label for="items">Add your selection</label> <select id="items" name="items"></select> </div> <!-- Buttons for items selected appear here --> <div id="items-selections"></div> </div> <!-- Comma separated list that creates item buttons. --> <div> <div> <label for="tags" >Add your tags <em>(separate by comma): </em></label ></label> <input type="text" name="tags" id="tags" /> </div> <div> <button id="addTag" type="button">Add</button> </div> <!-- Buttons for tag appear here. --> <div id="tags-selection"></div> |
Our next step will be inside the script tags of our HTML file. If you are importing just the input.js file into your HTML file you will need to invoke you script tags as a module :
1 2 3 |
<script type="module" src="input.js"> ... </script> |
If you are importing more than one file, like I am in the example, I recommend you import your Javascript files like this:
1 2 3 4 5 |
<script type="module"> import { items } from "./resources/items.js"; import { itemButton } from "./resources/input.js"; ... </script> |
In the example above, my two files are in the resources folder. The items.js file is just the file I have stored my list of select items.
Your next task is to add event listeners for each of your elements. For me, I will add an “input” event for my select element.
For my text input, I will add both a “keypress” (Enter) listener if the user hits enter after typing in their items in the “tags” text input or a “click” if they hit the “Add” button. We will also clear out the tags after each use.
Take a look at the example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/** * Add a item button for items. */ itemsLoc.addEventListener("input", () => { ... }); /** * Add an item button for tags. */ document.querySelector("#addTag").addEventListener("click", () => { let val = document.querySelector("#tags").value; ... document.querySelector("#tags").value = ""; }); document.querySelector("#tags").addEventListener("keypress", (e) => { if (e.key === "Enter") { let val = document.querySelector("#tags").value; ... document.querySelector("#tags").value = ""; } }); |
Now it’s time to add the button. We do this inside each event listener.
The add()
method takes three arguments:
items
– A string containing a single item or comma-separated list of items.selectionsLoc
– This is a string containing the ID reference of the location you want to display your buttons, usually in a div or span.numSelections
– The maximum total number of items you wish to have the user select.itemButton().add(items, selectionsLoc, numSelections)
Back to our example for our select element, our item is the value of the current selection. The location that we want to display our item buttons is theitems-selection
div and the maximum number of items that our user can add is 2.
1 2 3 4 5 6 |
/** * Add a item button for items. */ itemsLoc.addEventListener("input", () => { itemButton().add(itemsLoc.value, "items-selections", 2); }); |
For our text input, the value is the comma-separated string of values that the user enters. The location the button items will be displayed in will be the tags-selection
div and we will allow the user to add up to 10 items.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * Add an item button for tags. */ document.querySelector("#addTag").addEventListener("click", () => { let val = document.querySelector("#tags").value; itemButton().add(val, "tags-selection", 10); document.querySelector("#tags").value = ""; }); document.querySelector("#tags").addEventListener("keypress", (e) => { if (e.key === "Enter") { let val = document.querySelector("#tags").value; itemButton().add(val, "tags-selection", 10); document.querySelector("#tags").value = ""; } }); |
To get a list of item buttons from any of your assigned areas that display them, you can use the list()
method.
This method takes one argument, the element id that the item buttons are contained in. The list method will return an object of key-value pairs containing:
1 2 3 4 |
{ id: element id, val: the displayed value of the button } |
This example file setup is as follow:
Here is the sample HTML file below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Example</title> <link rel="stylesheet" type="text/css" href="main.css" media="screen" /> </head> <body> <div class="container"> <h1 align="center">Create removable button item lists from text input and select tags</h1> <!-- Selection menu used to create item buttons --> <div> <div> <label for="items">Add your selection</label> <select id="items" name="items"></select> </div> <!-- Buttons for items selected appear here --> <div id="items-selections"></div> </div> <!-- Comma separated list that creates item buttons. --> <div> <div> <label for="tags" >Add your tags <em>(separate by comma): </em></label ></label> <input type="text" name="tags" id="tags" /> </div> <div> <button id="addTag" type="button">Add</button> </div> <!-- Buttons for tag appear here. --> <div id="tags-selection"></div> </div> <hr> <button id="submit" type="button">Log Items</button> </form> </div> <script type="module"> //Bring in list of items file and the item button methods. import { items } from "./resources/items.js"; import { itemButton } from "./resources/input.js"; /** * Generate options for item selection menu. */ let options = `<option id="items-option1" disabled selected value>-- select an option --</option>`.concat( items .map((item) => { return `<option value="${item}">${item}</option>`; }) .join("\n") ); let itemsLoc = document.querySelector("#items"); itemsLoc.innerHTML = options; /** * Add a item button for items. */ itemsLoc.addEventListener("input", () => { itemButton().add(itemsLoc.value, "items-selections", 2); }); /** * Add a item button for tags. */ document.querySelector("#addTag").addEventListener("click", () => { let val = document.querySelector("#tags").value; itemButton().add(val, "tags-selection", 10); document.querySelector("#tags").value = ""; }); document.querySelector("#tags").addEventListener("keypress", (e) => { if (e.key === "Enter") { let val = document.querySelector("#tags").value; itemButton().add(val, "tags-selection", 10); document.querySelector("#tags").value = ""; } }); /** * Log all selected items in console. */ document.querySelector("#submit").addEventListener("click", () =>{ console.log("items-selections", itemButton().list("items-selections")); console.log("tags-selection", itemButton().list("tags-selection")); }) </script> </body> </html> |
And the resources > items.js file for the select element.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export const items = [ "Cheese", "Cake", "Bananas", "Pizza", "Donuts", "Steak", "Cucumber", "Meat Pies", "Peaches", "Hamburgers", "Asparagus", ]; |
I hope you found this small library useful for creating your own buttons. You may wish to make style changes to your buttons to match your own colour theme.
You may also wish to extend or reduce the length of characters for each item. You can do this in the input.js file on line 37.
You can download a copy of the example here:
I really like hearing how people apply these components to their own projects. Feel free to share in the comments below.
If you found this tool useful, please click the like button so I know that I am making good content. Or if you want to get updates on my latest posts, please subscribe (below the comments).
~Yagi