Categories
JavaScript Basics

Easily Iterate Over JavaScript Collections with the For-Of Loop

Starting with ES2015, we have a new kind of loop to loop over iterable objects. The new for…of loop is a new kind of loop that lets us…

Starting with ES2015, we have a new kind of loop to loop over iterable objects. The new for...of loop is a new kind of loop that lets us loop over any iterable objects without using a regular for loop, while loop, or using the forEach function in the case of arrays. It can be used directly to iterate through any iterable objects, which include built in objects like Strings, Arrays, array-like objects like arguments and NodeList , TypedArray , Map , Set and any user-defined iterables. User-defined iterables include entities like generators and iterators.

If we want to use the for...of loop to iterate over an iterable object, we can write it with the following syntax:

for (variable of iterable){
  // run code
}

The variable in the code above is the variable representing each entry of the iterable object that are being iterated over. It can be declared with const , let or var . The iterable is the object where the properties are being iterated over.

For example, we can use it to iterate over an array like in the following code:

const arr = [1,2,3];

for (const num of arr) {
  console.log(num);
}

The code above, the console.log statements will log 1, 2, and 3. We can replace const with let if we want to assign the variable we used for iteration in the for...of loop. For example, we can write:

const arr = [1,2,3];

for (let num of arr) {
  num *= 2 ;
  console.log(num);
}

The code above, the console.log statements will log 2, 4, and 6 since we used the let keyword to declare the num so we can modify num in place by multiplying each entry by 2. We cannot reassign with const so we have to use let or var to declare the variable we want to modify in each iteration.

We can also iterate over strings. If we do that we all get each character of the string in each iteration. For example, if we have the code below:

const str = 'string';

for (const char of str) {
  console.log(char);
}

Then we get the individual characters of 'string' logged in each line.

Likewise, we can iterate over TypedArrays, which contains binary data represented by a series of numbers in hexadecimal format. For example, we can write the following code:

const arr = new Uint8Array([0x00, 0x2f]);`

for (const num of arr) {
  console.log(num);
}`

In the example above, console.log will log 0 and 47. Note that the logged value is in decimal format but the entered value is in hexadecimal format.

If we iterate over Maps, then we get each entry of the Map. For example, we can write the following code to iterate over Maps:

const map = new Map([['a', 2], ['b', 4], ['c', 6]]);

for (const entry of map) {
  console.log(entry);
}

If we log the entries, we get ['a', 2], ['b', 4], and ['c', 6] . Maps consists of key-value pairs as their entries. When we iterate over a Map, we get the key as the first element and the value as the second element is each entry. To get the key and value of each entry into its own variable we can use the destructuring operator, like in the following code:

const map = new Map([['a', 2], ['b', 4], ['c', 6]]);

for (const [key, value] of map) {
  console.log(key, value);
}

Then when we log the entries, we get 'a' 2, 'b' 4, and 'c' 6 .

We can also use the for...of loop for Sets. For example, we can loop over a Set by doing the following:

const set = new Set([1, 1, 2, 3, 3, 4, 5, 5, 6]);

for (const value of set) {
  console.log(value);
}

We set that we get 1, 2, 3, 4, 5, and 6 logged since the Set constructor automatically eliminates duplicate entries by keeping the first occurrence a value in the Set and discarding the later occurrence of the same value.

The for...of loop also works for iterating over the arguments object, which is an global object that has the arguments that were passed into the function when the function is called. For example, if we write the following code:

(function() {
  for (const argument of arguments) {
    console.log(argument);
  }
})(1, 2, 3, 4, 5, 6);

We see that we see 1, 2, 3, 4, 5, and 6 logged since this is what we passed in when we called the function. Note that this only works for regular functions since the context of this has to be changed to the function being called instead of window . Arrow functions doesn’t change the content of this , so we won’t get the correct arguments when we run the same loop inside an arrow function.

Also, we can iterate over a list of DOM Node objects, called a NodeList . For example, is a browser implemented the NodeList.prototype[Symbol.iterator] , then we can use the for...of loop like in the following code:

const divs = document.querySelectorAll('div');

for (const div of divs) {
  console.log(div);
}

In the code above we logged all the div elements that are in the document.

With for...of loops, we can end the loop by using the break , throw or return statements. The iterator will close in this case, but the execution will continue outside the loop. For example, if we write:

function* foo(){
  yield 'a';
  yield 'b';
  yield 'c';
};

for (const o of foo()) {
  console.log(o);
  break;
}

console.log('finished');

In the code above, we only log ‘a’ because we have a break statement at the end of the for...of loop, so after the first iteration, the iterator will close and the loop ends.

We can loop over generators, which are special functions that returns a generator function. The generator function returns the next value of an iterable object. It’s used for letting us iterate through a collection of objects by using the generator function in a for...of loop.

We can also loop over a generator that generate infinite values. We can have an infinite loop inside the generator to keep returning new values. Because the yield statement doesn’t run until the next value is requested, we can keep an infinite loop running without crashing the browser. For example, we can write:

function* genNum() {
  let index = 0;
  while (true) {
    yield index += 2;
  }
}

const gen = genNum();
for (const num of gen) {
  console.log(num);
  if (num >= 1000) {
    break;
  }
}

If we run the code above, we see that we get numbers from 2 to 1000 logged. Then num is bigger than 1000, so that the break statement is ran. We cannot reuse the generator after it’s closed, so if we write something like the following:

function* genNum() {
  let index = 0;
  while (true) {
    yield index += 2;
  }
}

const gen = genNum();
for (const num of gen) {
  console.log(num);
  if (num >= 1000) {
    break;
  }
}

for (const num of gen) {
  console.log(num);
  if (num >= 2000) {
    break;
  }
}

The second loop won’t run because the iterator that was generated by the generator is already closed by the first loop with the break statement.

We can iterate over other iterable objects that have the method denoted with the Symbol.iterator Symbol defined. For example, if we have the following iterable object defined:

const numsIterable = {
  [Symbol.iterator]() {
    return {
      index: 0,
      next() {
        if (this.index < 10) {
          return {
            value: this.index++,
            done: false
          };
        }
        return {
          value: undefined,
          done: true
        };
      }
    };
  }
};

Then we can run the loop below to show log the generated results:

for (const value of numsIterable) {
  console.log(value);
}

When we run it, should see 0 to 9 logged when console.log is run in the loop above.

It’s important that we don’t confuse the for...of loop with the for...in loop. The for...in loop if for iterating over the top-level keys of objects including anything up the prototype chain, while the for...of loop can loop over any iterable object like arrays, Sets, Maps, the arguments object, the NodeList object, and any user-defined iterable objects.

For example, if we have something like:

Object.prototype.objProp = function() {};
Array.prototype.arrProp = function() {};

const arr = [1, 2, 3];
arr.foo = 'abc';

for (const x in arr) {
  console.log(x);
}

Then we get 0, 1, 2, ‘foo’, ‘arrProp’ and ‘objProp’ logged, which are keys of objects and methods that are defined for the arr object. It included all properties and methods up the prototype chain. It inherited all properties and methods from Object and Array that were added to Object and Array’s prototype so we get all the things in the chain inheritance in the for...in loop. Only enumerable properties are logged in the arr object in arbitrary order. It logs index and properties we defined in Object and Array like objProp and arrProp .

To only loop through properties that aren’t inheritance from an object’s prototype, we can use the hasOwnPropetty to check if the property is defined on the own object:

Object.prototype.objProp = function() {};
Array.prototype.arrProp = function() {};

const arr = [1, 2, 3];
arr.foo = 'abc';

for (const x in arr) {
  if (arr.hasOwnProperty(x)){
    console.log(x);
  }
}

objProp and arrProp are omitted because they’re they’re inherited from Object and Array objects respectively.

The for...of loop is a new kind of loop that lets us loop over any iterable objects without using a regular for loop, while loop, or using the forEach function in the case of arrays. It can be used directly to iterate through any iterable objects, which include built in objects like Strings, Arrays, array-like objects like arguments and NodeList , TypedArray , Map , Set and any user-defined iterables. User-defined iterables include entities like generators and iterators. This is a handy loop because it lets us over any iterable object rather than just arrays. Now we have a loop statement that works with iterable object.

Categories
JavaScript

Storing Key-Value Pairs With JavaScript Maps

With ES2015, we have a new data structure to store key-value pairs, called Maps, which are dictionaries that we can use to store any object as keys and any object as values.

Before we had Maps, we had to use objects as dictionaries that only use strings as keys, but values could be anything.

Using Maps is simple, we can define Maps like in the following code:

let messageMap = new Map();  
messageMap.set('hi', 'Hello');  
messageMap.set('greeting', 'How are you doing?');  
messageMap.set('bye', 'Bye');

Instead of using the set method to add our keys and values, we can also pass a nested array where each entry of the array has the key as the first element and the value as the second element.

For example, we can write:

const arrayOfKeysAndValues = [  
  ['a', 1],  
  ['b', 2],  
  ['c', 3],  
  ['d', 3],  
  ['e', 4],  
  ['f', 5],  
  ['g', 6],  
  ['h', 7],  
  ['i', 8],  
  ['j', 9],  
  ['k', 10],  
  ['l', 11],  
  ['m', 12],  
  ['n', 13],  
  ['o', 14],  
]  
let numMap = new Map(arrayOfKeysAndValues);  
console.log(numMap)

The code above will create a Map with the first element of each array as the key and the second element of each array as the corresponding value. So, after running the console.log line, we get all the entries of the Map listed in the same order as the array.

We can get the number of key-value pairs defined in a Map by using the size property:

messageMap.size // 3

To get the value by the key, we use the get function:

messageMap.get('hi'); // 'Hello'  
messageMap.get('hello'); // undefined

It returns the value if the key exists and undefined if an entry with the key doesn’t exist.

To remove an entry given the key, we can use the delete function with the key passed in:

messageMap.delete('hi');

If we call set with the same key passed in more than once, then the value in the later set call overwrites the earlier one. For example:

let messageMap = new Map();  
messageMap.set('hi', 'Hello');  
messageMap.set('greeting', 'How are you doing?');  
messageMap.set('bye', 'Bye');  
messageMap.set('hi', 'Hi');
console.log(messageMap.get('hi'))

If we run console.log(messageMap.get(‘hi’)), we get 'Hi' instead of 'Hello'.

To clear all the entries of a Map object, we can call the clear function, like in the following code:

let messageMap = new Map();  
messageMap.set('hi', 'Hello');  
messageMap.set('greeting', 'How are you doing?');  
messageMap.set('bye', 'Bye');  
messageMap.set('hi', 'Hi');  
messageMap.clear();  
console.log(messageMap)  
console.log(messageMap.size)

We see that messageMap should be empty when we log it and that its size should be 0.

The entries of a Map can be iterated over with a for...of loop. With the destructuring assignment of each entry, we can get the key and value of each entry with a for...of loop like in the following code:

let messageMap = new Map();  
messageMap.set('hi', 'Hello');  
messageMap.set('greeting', 'How are you doing?');  
messageMap.set('bye', 'Bye');

for (let [key, value] of messageMap) {  
  console.log(`${key} - ${value}`);  
}

Objects and Maps are different in multiple ways. The keys of objects can only be of the string type. In Maps, keys can be of any value.

We can get the size of a Map easily with the size property which isn’t available in objects. The iteration order of Maps is in the iteration order of the elements, while the iteration of the keys of an object isn’t guaranteed.

Object has a prototype, so there are default keys in an object, but not in a Map.

Maps are preferred is we want to store non-string keys, when keys are unknown until run time, and when all the keys are the same type and values are the same type which may or may not be the same type as the keys.

We can use the entries function to get the entries of a Map. For example, we can write:

let messageMap = new Map();  
messageMap.set('hi', 'Hello');  
messageMap.set('greeting', 'How are you doing?');  
messageMap.set('bye', 'Bye');

for (let [key, value] of messageMap.entries()) {  
  console.log(`${key} - ${value}`);  
}

To loop through the entries of the Map.

Keys are checked for equality by mostly following the rules of the triple equals operator, except that NaN is considered equal to itself and +0 and -0 are also considered equal. The algorithm is called the same-value-zero algorithm.

So, if we have:

const map = new Map();  
map.set(NaN, 'abc');

Then we run map.get(NaN), we get that 'abc' returned. Also, it’s important to note that we can use non-string values as keys like we did above. For example:

let messageMap = new Map();  
messageMap.set(1, 'Hello');  
messageMap.set(2, 'How are you doing?');  
messageMap.set(3, 'Bye');

If we call messageMap.get(1), we get 'Hello'.

However, since the keys are retrieved by checking with the triple equal operator with NaN considered equal to itself, we cannot get the value that you expect directly from object keys.

let messageMap = new Map();  
messageMap.set({ messageType: 'hi' }, 'Hello');  
messageMap.set({ messageType: 'greeting' }, 'How are you doing?');  
messageMap.set({ messageType: 'bye' }, 'Bye');  
const hi = messageMap.get({  
  messageType: 'hi'  
})
console.log(hi);

hi will be undefined as it doesn’t check the content of the object for equality.

We can use array methods on Map if we convert it to an array first and then convert it back to a Map. For example, if we want to multiply all the values in a Map by 2, then we can write the following:

let numMap = new Map();  
numMap.set('a', 1);  
numMap.set('b', 2);  
numMap.set('c', 3);

let multipliedBy2Array = [...numMap].map(([key, value]) => ([key, value * 2]));

let newMap = new Map(multipliedBy2Array);
console.log(newMap)

As we can see from the code above, we can use the spread operator to convert a Map to an Array directly by copying the values from the Map to the array.

Each entry consists of an array with the key as the first element and the value as the second element. Then we can call map on it since it’s an array. We can then multiply each value’s entry by 2, while keeping each entry’s array structure the same, with key first and value second.

Then we can pass it directly to the constructor of the Map object and we can see that the new Map object, newMap, has all the values multiplied by 2 while keeping the same keys as numMap.

Likewise, we can combine multiple Maps by first putting them in the same array with the spread operator and combining them into one, then passing it to the Map constructor to generate a new Map object.

For example, we can write:

let numMap1 = new Map();  
numMap1.set('a', 1);  
numMap1.set('b', 2);  
numMap1.set('c', 3);  
numMap1.set('d', 3);  
numMap1.set('e', 4);  
numMap1.set('f', 5);  
numMap1.set('g', 6);  
numMap1.set('h', 7);  
numMap1.set('i', 8);  
numMap1.set('j', 9);  
numMap1.set('k', 10);  
numMap1.set('l', 11);let numMap2 = new Map();  
numMap2.set('m', 1);  
numMap2.set('n', 2);  
numMap2.set('o', 3);  
numMap2.set('p', 3);  
numMap2.set('q', 4);  
numMap2.set('r', 5);  
numMap2.set('s', 6);  
numMap2.set('t', 7);  
numMap2.set('u', 8);  
numMap2.set('v', 9);  
numMap2.set('w', 10);  
numMap2.set('x', 11);let numMap3 = new Map();  
numMap3.set('y', 1);  
numMap3.set('z', 2);  
numMap3.set('foo', 3);  
numMap3.set('bar', 3);  
numMap3.set('baz', 4);

const combinedArray = [...numMap1, ...numMap2, ...numMap3];  
const combinedNumMap = new Map(combinedArray);

When we log combinedNumMap, we see that we have all the original keys and values intact, but they were combined into one Map.

If we have overlapping keys, then the one that’s inserted later overwrites the values that were inserted earlier. For example:

let numMap1 = new Map();  
numMap1.set('a', 1);  
numMap1.set('b', 2);  
numMap1.set('c', 3);  
numMap1.set('d', 3);  
numMap1.set('e', 4);  
numMap1.set('f', 5);  
numMap1.set('g', 6);  
numMap1.set('h', 7);  
numMap1.set('i', 8);  
numMap1.set('j', 9);  
numMap1.set('k', 10);  
numMap1.set('l', 11);

let numMap2 = new Map();  
numMap2.set('m', 1);  
numMap2.set('n', 2);  
numMap2.set('o', 3);  
numMap2.set('p', 3);  
numMap2.set('q', 4);  
numMap2.set('r', 5);  
numMap2.set('s', 6);  
numMap2.set('t', 7);  
numMap2.set('u', 8);  
numMap2.set('v', 9);  
numMap2.set('w', 10);  
numMap2.set('x', 11);let numMap3 = new Map();  
numMap3.set('y', 1);  
numMap3.set('z', 2);  
numMap3.set('a', 21);  
numMap3.set('b', 22);  
numMap3.set('c', 23);

const combinedArray = [...numMap1, ...numMap2, ...numMap3];  
const combinedNumMap = new Map(combinedArray);

When we log combinedNumMap, we see that we have most of the original keys and values intact, but they were combined into one Map. However, a now maps to 21, b now maps to 22, and c now maps to 23.

With Maps, we can have key-value pairs where the keys aren’t strings. Maps are collections that can be iterated in the order that the key-value pairs are inserted in.

They can be converted to arrays with each entry being an array with the key as the first element and the value as the second element, which means that we can convert them to arrays and then use arras operations like array methods.

We can use the spread operator to manipulate them in the way that we can do with any arrays, then convert them back to Maps again. We can use the set function to add more entries, and the get function to get the value of the given key.

The values are retrieved by the keys by the same-value-zero algorithm which means the keys are matched by the triple equals operator, with the exception that -0 and +0 are considered equal and NaN is considered equal to itself.

Categories
JavaScript JavaScript Basics

Comparing Non-English Strings with JavaScript Collators

With the combination of the double equal or triple equal operator with string methods, we can compare strings easily in a case-sensitive or case insensitive manner. However, this doesn’t take into account the characters that are in non-English strings like French or Italian. These languages have alphabets that may contain accents, something that isn’t recognized in normal string comparisons.

To handle this scenario, we can use the Intl.Collator object to compare strings with accents or for different locales. The Intl.Collator object is a constructor for collators, which are objects that let us compare characters in a language-sensitive way. With Collators, we can compare the order of single characters according to the language that is specified.

Basic Collator Usage for String Equality Comparison

To use a collator, we can construct a Collator object and then use its compare method. The compare method does a comparison of the alphabetical order of the entire string based on the locale. For example, if we want to compare two strings in the German using its alphabet’s order, we can write the following code:

const collator = new Intl.Collator('de');  
const order = collator.compare('Ü', 'ß');  
console.log(order);

We created the Collator object by writing new Intl.Collator(‘de’) to specify that we are comparing strings in the German alphabet. Then we use the created compare method, which takes two parameters as the two strings that you want to compare in string form.

Then a number is returned from the compare function. 1 is returned if the string in the first parameter comes after the second one alphabetically, 0 if both strings are the same, and -1 is returned if the string in the first parameter comes before the second string alphabetically.

So if we flip the order of the strings like in the code below:

const collator = new Intl.Collator('de');  
const order = collator.compare('ß', 'Ü');  
console.log(order);

Then the console.log outputs -1.

If they’re the same, like in the following code:

const collator = new Intl.Collator('de');  
const order = collator.compare('ß', 'ß');  
console.log(order);

Then we get 0 returned for order.

To summarize: If the strings are equal, the function returns 0. If they are not equal the function returns either 1 or -1 which also indicates the alphabetical order of the strings.

Advanced Usage

The Collator is useful because we can put it in the Array.sort method as a callback function to sort multiple strings in the array. For example, if we have multiple German strings in an array, like in the code below:

const collator = new Intl.Collator('de');  
const sortedLetters = ['Z', 'Ä', 'Ö', 'Ü', 'ß'].sort(collator.compare);  
console.log(sortedLetters);

Then we get [“Ä”, “Ö”, “ß”, “Ü”, “Z”].

The constructor takes a number of options that take into account the features of the alphabets of different languages. As we can see above, the first parameter in the constructor is the locale, which is BCP-47 language tag, or an array of such tags. This is an optional parameter. An abridged list of BCP-47 language tags include:

  • ar — Arabic
  • bg — Bulgarian
  • ca — Catalan
  • zh-Hans — Chinese, Han (Simplified variant)
  • cs — Czech
  • da — Danish
  • de — German
  • el — Modern Greek (1453 and later)
  • en — English
  • es — Spanish
  • fi — Finnish
  • fr — French
  • he — Hebrew
  • hu — Hungarian
  • is — Icelandic
  • it — Italian
  • ja — Japanese
  • ko — Korean
  • nl — Dutch
  • no — Norwegian
  • pl — Polish
  • pt — Portuguese
  • rm — Romansh
  • ro — Romanian
  • ru — Russian
  • hr — Croatian
  • sk — Slovak
  • sq — Albanian
  • sv — Swedish
  • th — Thai
  • tr — Turkish
  • ur — Urdu
  • id — Indonesian
  • uk — Ukrainian
  • be — Belarusian
  • sl — Slovenian
  • et — Estonian
  • lv — Latvian
  • lt — Lithuanian
  • tg — Tajik
  • fa — Persian
  • vi — Vietnamese
  • hy — Armenian
  • az — Azerbaijani
  • eu — Basque
  • hsb — Upper Sorbian
  • mk — Macedonian
  • tn — Tswana
  • xh — Xhosa
  • zu — Zulu
  • af — Afrikaans
  • ka — Georgian
  • fo — Faroese
  • hi — Hindi
  • mt — Maltese
  • se — Northern Sami
  • ga — Irish
  • ms — Malay (macrolanguage)
  • kk — Kazakh
  • ky — Kirghiz
  • sw — Swahili (macrolanguage)
  • tk — Turkmen
  • uz — Uzbek
  • tt — Tatar
  • bn — Bengali
  • pa — Panjabi
  • gu — Gujarati
  • or — Oriya
  • ta — Tamil
  • te — Telugu
  • kn — Kannada
  • ml — Malayalam
  • as — Assamese
  • mr — Marathi
  • sa — Sanskrit
  • mn — Mongolian
  • bo — Tibetan
  • cy — Welsh
  • km — Central Khmer
  • lo — Lao
  • gl — Galician
  • kok — Konkani (macrolanguage)
  • syr — Syriac
  • si — Sinhala
  • iu — Inuktitut
  • am — Amharic
  • tzm — Central Atlas Tamazight
  • ne — Nepali
  • fy — Western Frisian
  • ps — Pushto
  • fil — Filipino
  • dv — Dhivehi
  • ha — Hausa
  • yo — Yoruba
  • quz — Cusco Quechua
  • nso — Pedi
  • ba — Bashkir
  • lb — Luxembourgish
  • kl — Kalaallisut
  • ig — Igbo
  • ii — Sichuan Yi
  • arn — Mapudungun
  • moh — Mohawk
  • br — Breton
  • ug — Uighur
  • mi — Maori
  • oc — Occitan (post 1500)
  • co — Corsican
  • gsw — Swiss German
  • sah — Yakut
  • qut — Guatemala
  • rw — Kinyarwanda
  • wo — Wolof
  • prs — Dari
  • gd — Scottish Gaelic

For example, de is for German or fr-ca for Canadian French. So, we can sort Canadian French strings by running the following code:

const collator = new Intl.Collator('fr-ca');  
const sortedLetters = ['ç', 'à', 'c'].sort(collator.compare);  
console.log(sortedLetters);

The constructor to Collator can also take an array of strings for multiple locale comparison — new Intl.Collator([/* local strings */]). The array argument allows us to sort strings from multiple locales. For example, we can sort both Canadian French alphabet and the German alphabet at the same time:

const collator = new Intl.Collator(['fr-ca', 'de']);  
const sortedLetters = [  
  'Ü', 'ß', 'ç', 'à', 'c'  
].sort(collator.compare);
console.log(sortedLetters);

Then we get [“à”, “c”, “ç”, “ß”, “Ü”] from the console.log statement.

Additional Options

Unicode extension keys which include "big5han", "dict", "direct", "ducet", "gb2312", "phonebk", "phonetic", "pinyin", "reformed", "searchjl", "stroke", "trad", "unihan" are also allowed in our locale strings. They specify the collations that we want to compare strings with. However, when there are fields in the options in the second argument that overlaps with this, then the options in the argument overrides the Unicode extension keys specified in the first argument.

Numerical collations can be specified by adding kn to your locale string in your first argument. For example, if we want to compare numerical strings, then we can write:

const collator = new Intl.Collator(['en-u-kn-true']);  
const sortedNums = ['10', '2'].sort(collator.compare);  
console.log(sortedNums);

Then we get [“2”, “10”] since we specified kn in the locale string in the constructor which makes the collator compare numbers.

Also, we can specify whether upper or lower case letters should be sorted first with the kf extension key. The possible options are upper, lower, or false. false means that the locale’s default will be the option. This option can be set in the locale string by adding as a Unicode extension key, and if both are provided, then the option property will take precedence. For example, to make uppercase letters have precedence over lowercase letters, we can write:

const collator = new Intl.Collator('en-ca-u-kf-upper');  
const sorted = ['Able', 'able'].sort(collator.compare);  
console.log(sorted);

This sorts the same word with upper case letters first. When we run console.log, we get [“Able”, “able”] since we have an uppercase ‘A’ in ‘Able’, and a lowercase ‘a’ for ‘able’. On the other hand, if we instead pass in en-ca-u-kf-lower in the constructor like in the code below:

const collator = new Intl.Collator('en-ca-u-kf-lower');  
const sorted = ['Able', 'able'].sort(collator.compare);  
console.log(sorted);

Then after console.log we get [“able”, “Able”] because kf-lower means that we sort the same word with lowercase letters before the ones with uppercase letters.

The second argument of the constructor takes an object that can have multiple properties. The properties that the object accepts are localeMatcher, usage, sensitivity, ignorePunctuation, numeric, and caseFirst. numeric is the same as the kn option in the Unicode extension key in the locale string, and caseFirst is the same as the kf option in the Unicode extension key in the locale string. The localeMatcher option specifies the locale matching algorithm to use. The possible values are lookup and best fit. The lookup algorithm searches for the locale until it finds the one that fits the character set of the strings that are being compared. best fit finds the locale that is at least but possibly more suited that the lookup algorithm.

The usage option specifies whether the Collator is used for sorting or searching for strings. The default option is sort.

The sensitivity option specifies the way that the strings are compared. The possible options are base, accent, case, and variant.

base compares the base of the letter, ignoring the accent. For example a is not the same as b, but a is the same as á, a is the same as Ä.

accent specifies that a string is only different if there is a base letter or their accents are unequal then they’re unequal, ignoring case. So a isn’t the same as b, but a is the same as A. a is not the same as á.

The case option specifies that strings that are different in their base letters or case are considered unequal, so a wouldn’t be the same as A and a wouldn’t be the same as c, but a is the same as á.

variant means that strings that are different in the base letter, accent, other marks, or case are considered unequal. For example a wouldn’t be the same as A and a wouldn’t be the same as c. But also a wouldn’t be the same as á.

The ignorePunctuation specifies whether punctuation should be ignored when sorting strings. It’s a boolean property and the default value is false.

We can use the Collator constructor with the second argument in the following way:

const collator = new Intl.Collator('en-ca', {  
  ignorePunctuation: false,  
  sensitivity: "base",  
  usage: 'sort'  
});  
console.log(collator.compare('Able', 'able'));

In the code above, we sort by checking for punctuation and only consider letters different if the base letter is different, and we keep the default that upper case letters are sorted first, so we get [‘Able’, ‘able’] in the console.log.

We can search for strings as follows:

const arr = ["ä", "ad", "af", "a"];  
const stringToSearchFor = "af";
const collator = new Intl.Collator("fr", {  
  usage: "search"  
});  
const matches = arr.filter((str) => collator.compare(str, stringToSearchFor) === 0);  
console.log(matches);

We set the usage option to search to use the Collator to search for strings and when the compare method returns 0, then we know that we have the same string. So we get [“af”] logged when we run console.log(matches).

We can adjust the options for comparing letter, so if we have:

const arr = ["ä", "ad", "ef", "éf", "a"];  
const stringToSearchFor = "ef";
const collator = new Intl.Collator("fr", {  
  sensitivity: 'base',  
  usage: "search"  
});
const matches = arr.filter((str) => collator.compare(str, stringToSearchFor) === 0);
console.log(matches);

Then we get [“ef”, “éf”] in our console.log because we specified sensitivity as base which means that we consider the letters with the same base accent as the same.

Also, we can specify the numeric option to sort numbers. For example, if we have:

const collator = new Intl.Collator(['en-u-kn-false'], {  
  numeric: true  
});  
const sortedNums = ['10', '2'].sort(collator.compare);  
console.log(sortedNums);

Then we get [“2”, “10”] because the numeric property in the object in the second argument overrides the kn-false in the first argument.

Conclusion

JavaScript offers a lot of string comparison options for comparing strings that aren’t in English. The Collator constructor in Intl provides many options to let us search for or sort strings in ways that can’t be done with normal comparison operators like double or triple equals. It lets us order numbers, and consider cases, accents, punctuation, or the combination of those features in each character to compare strings. Also, it accepts locale strings with key extensions for comparison.

All of these options together make JavaScript’s Collator constructor a great choice for comparing international strings.

Categories
JavaScript JavaScript Basics

Using the JavaScript Math Object for Calculations

In JavaScript, the Math object is a built-in object that lets us do various mathematical operations.

It’s a non-function object with mathematical constants and functions to calculate various quantities. It only works with variables with the number type, and it doesn’t work with BigInt data.

The Math object isn’t a constructor. All the methods are static. We can refer to the members with the dot notation like Math.PI to get the value of Pi, or call the cosine function with Math.cos(x).

Constants are defined with full precision allowing for real numbers in JavaScript.


Constants

The Math object has the following constants:

Math.E

Euler’s constant and the base of natural logarithms, approximately 2.718281828459045.

Math.LN2

Natural logarithm of 2, approximately 0.6931471805599453.

Math.LN10

Natural logarithm of 10, approximately 2.302585092994046.

Math.LOG2E

Base 2 logarithm of E, approximately 1.4426950408889634.

Math.LOG10E

Base 10 logarithm of E, approximately 0.4342944819032518.

Math.PI

The ratio of the circumference of a circle to its diameter, approximately 3.141592653589793.

Math.SQRT1_2

The square root of 1/2, approximately 0.7071067811865476.

Math.SQRT2

The square root of 2, approximately 1.4142135623730951.


Methods

The Math object has many methods, like trigonometric functions, power functions, and logarithmic functions.

The trigonometric functions like sin(), cos(), tan(), asin(), acos(), atan(), atan2() expect angles in radians as arguments and also return angles in radians.

To convert radians to degrees, divide by (Math.PI / 180), and multiply by this to convert the other way.

The precision of the results are browser-dependent because each browser engine implements floating-point calculations differently. This means that different browsers may return different results for the same function.

Math.abs(x)

Returns the absolute value of a number. For example, we can use it as follows:

Math.abs(2) // returns 2  
Math.abs(-2) // returns 2

Math.acos(x)

Returns the inverse cosine of a number. For example, we can use it as follows:

Math.acos(1) // returns 0  
Math.acos(-1) // returns Math.PI

Math.acosh(x)

Returns the inverse hyperbolic cosine of a number. For example, we can use it as follows:

Math.acosh(1) // returns 0

Math.asin(x)

Returns the inverse sine of a number. For example, we can use it as follows:

Math.asin(1) // returns 1.5707963267948966  
Math.asin(0) // returns 0

Math.asinh(x)

Returns the inverse hyperbolic sine of a number. For example, we can use it as follows:

Math.asin(1) // returns 0.881373587019543  
Math.asin(0) // returns 0

Math.atan(x)

Returns the inverse tangent of a number. For example, we can use it as follows:

Math.atan(1) // returns 0.7853981633974483  
Math.atan(0) // returns 0

Math.atanh(x)

Returns the inverse hyperbolic tangent of a number. For example, we can use it as follows:

Math.atanh(1) // returns Infinity  
Math.atanh(0) // returns 0

Math.atan2(y, x)

Returns the inverse tangent of the quotient of its arguments. For example, we can use it as follows:

Math.atan2(1, 1) // returns 0.7853981633974483  
Math.atan2(1, Math.SQRT2) // returns 0.9553166181245093

Math.cbrt(x)

Returns the cube root of a number. For example, we can use it as follows:

Math.cbrt(3) // returns 1.4422495703074083

Math.ceil(x)

Returns the smallest integer greater than or equal to a number. For example, we can use it as follows:

Math.ceil(1.5) // returns 2

Math.clz32(x)

Returns the number of leading zeroes of a 32-bit integer when x is converted to binary. For example, if we write:

Math.clz32(1)

We get 31 because the 1s digit is 1 and the other 31 digits before it are all 0. And if we write:

Math.clz32(2)

We get 30 because the 1s digit is 0, the 2s digit is 1, and the other 30 digits before it are all 0.

Math.cos(x)

Returns the cosine of a number. For example, we can use it as follows:

Math.cos(0) // returns 1

Math.cosh(x)

Returns the hyperbolic cosine of a number. For example, we can use it as follows:

Math.cosh(0) // returns 1

Math.exp(x)

Returns e to the power of x, where x is the argument, and e is Euler’s constant which is approximately 2.718281828459045, and it’s the base of the natural logarithm. We can use it as in the following code:

Math.exp(10) // returns 22026.465794806718

Math.expm1(x)

Returns Math.exp(x) with 1 subtracted from it. For example, we can use it as in the following code:

Math.expm1(1) // returns 1.718281828459045

Math.floor(x)

Returns the largest integer less than or equal to a number. For example, we can use it as in the following code:

Math.floor(1.1) // returns 1

Math.fround(x)

Returns the nearest single-precision float representation of a number.

A single-precision number has 32 bits, with the first bit used for the sign, the next 8 bits used for the exponent, and the remaining 23 bits are the fractional parts of the logarithm, also called the mantissa.

For example, we can use it as in the following code:

Math.fround(1.2) // returns 1.2000000476837158

Math.hypot(a,b,c,…)

Returns the square root of the sum of squares of its arguments. It takes an infinite number of arguments. For example, we can use it as follows:

Math.hypot(1,2)  // returns 2.23606797749979  
Math.hypot(1,2,3,4,5)  // returns 7.416198487095663

Math.imul(x, y)

Returns the result of 32-bit integer multiplication. It rounds off decimals to the nearest integer before performing the multiplication. For example, we can use it as follows:

Math.imul(1,2) // returns 2  
Math.imul(3,4.1) // returns 12

Math.log(x)

Returns the natural logarithm (log with base e, also ln) of a number. For example, we can use it as the following code:

Math.log(Math.E) // returns 1  
Math.log(1) // returns 0

Math.log1p(x)

Returns the natural logarithm (log with base e, also ln) of 1 + x for a number x. For example, we can use it like the following code:

Math.log1p(Math.E - 1) // returns 1  
Math.log1p(1) // returns 0.6931471805599453

Math.log10(x)

Returns the base 10 logarithm of a number. For example, we can use it like the following code:

Math.log10(1) // returns 0  
Math.log10(10) // returns 1

Math.log2(x)

Returns the base 2 logarithm of a number. For example, we can use it like the following code:

Math.log2(2) // returns 0  
Math.log2(10) // returns 3.321928094887362

Math.max(a,b,…)

Returns the largest of zero or more numbers. If nothing is passed in, -Infinity is returned. For example, we can use it like the following code:

Math.max(1, 2) // returns 2  
Math.max(1,2,3,4,5) // returns 5  
Math.max() // returns -Infinity

Math.min(a,b,…)

Returns the smallest of zero or more numbers. If nothing is passed in, Infinity is returned. For example, we can use it like the following code:

Math.min(1,2) // returns 1  
Math.min(1,2,3,4,5) // returns 1  
Math.min() // returns Infinity

Math.pow(x, y)

Returns base to the exponent power. For example, we can use it like the following code:

Math.pow(1,2) // returns 1  
Math.min(2,3) // returns 8

Math.random()

Returns a pseudo-random number between 0 and 1. For example, we can use it as the following code:

Math.random() // returns 0.5275086314071882 or any other number between 0 and 1

Math.round(x)

Returns the value of a number rounded to the nearest integer. For example, we can use it as the following code:

Math.round(1.2) // returns 1

Math.sign(x)

Returns the sign of the x, indicating whether x is positive, negative, or zero. If x is positive, 1 is returned. If an x is negative, then -1 is returned. If x is 0 then 0 is returned. For example, we can use it like in the following code:

Math.sign(1) // returns 1  
Math.sign(3) // returns 1  
Math.sign(-1) // returns -1  
Math.sign(-3) // returns -1  
Math.sign(0) // returns 0

Math.sin(x)

Returns the sine of a number. For example, we can use it as in the following code:

Math.sin(1) // returns 0.8414709848078965

Math.sinh(x)

Returns the hyperbolic sine of a number. For example, we can use it as in the following code:

Math.sinh(1) // returns 1.1752011936438014

Math.sqrt(x)

Returns the positive square root of a number. For example, we can use it like in the following code:

Math.sqrt(1) // returns 1  
Math.sqrt(2) // returns 1.4142135623730951

Math.tan(x)

Returns the tangent of a number. For example, we can use it as in the following code:

Math.tan(1) // returns 1.5574077246549023

Math.tanh(x)

Returns the hyperbolic tangent of a number. For example, we can use it as in the following code:

Math.tanh(2) // returns 0.9640275800758169

Math.trunc(x)

Returns the integer part of the number x, removing any fractional digits. For example, we can use it as in the following code:

Math.trunc(2.2223) // returns 2

Extending the Math Object

We can extend the Math object by adding custom properties and methods to it since it’s not a constructor. For instance, we can add the following function to the Math object to calculate the fifth root of a number:

Math.fifthRoot = (x) => Math.pow(x, 1/5);

The Math object is handy for making calculations in JavaScript. It has some useful constants and methods for calculations of common math operations like taking the logarithm and trigonometric functions.

We can extend the Math object easily by attaching methods and properties to it directly.

Categories
JavaScript

Introduction to JavaScript Data Types

JavaScript, like any other programming language, has its own data structures and types. JavaScript has a few data types that we have to know about in order to build programs with it. Different pieces of data can be put together to build more complex data structures.

JavaScript is a loosely typed, or dynamically typed, language. This means that a variable that’s declared with one type can be converted to another type without explicitly converting the data to another type. Variables can also contain any type at any time, depending on what’s assigned. For example, if we write the following code:

let x = 1;      
x = 'bar';  
x = true;

In the first line x is a number, but in the second line, the same variable x has been reassigned into a string. In the last line, it has been again been reassigned, this time to a Boolean.

JavaScript has multiple data types. There are seven primitive data types and an object type. The seven primitive types are Boolean, null, undefined, number, BigInt, string, and symbol.

All of JavaScript’s primitive types are immutable, which means that they can’t be changed. The primitive types contain values that are fixed once they’re defined. The Boolean type is either true or false and represents logical entities. The null type only has one value, which is null. The null value means that it refers to some nonexistent or invalid object or address. The undefined type is unique to JavaScript. It means that a variable hasn’t been assigned any value.


Numbers

There are two number types in JavaScript, which are number and BigInt. The number type is a double-precision 64-bit number that can have values between -2 to the 53rd power minus 1 and 2 to the 53rd power minus 1. There’s no specific type for integers. All numbers are floating point numbers. There are also three symbolic values: Infinity , -Infinity and NaN.

The largest and smallest available values for a number are Infinity and -Infinity respectively. We can also use the constants Number.MAX_VALUE or Number.MIN_VALUE to represent the largest and smallest numbers. We can use the Number.isSafeInteger() function to check whether a number is in the range of numbers available that are allowed in JavaScript. There is also the constants Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_NUMBER to check if the number you specify in the safe range. Anything outside the range isn’t safe and will be a double-precision floating-point of the value. The number 0 has two representations in JavaScript: There’s +0 and -0, and 0 is an alias for +0. It will be noticed if you try to divide a number by 0:

1/+0 // Infinity  
1/-0 // -Infinity

Sometimes numbers can represent Boolean values with bitwise operators to operate them as Boolean, but this is bad practice since JavaScript already has Boolean types, so using numbers to represent Boolean will be unclear to people reading the code. That’s because numbers can represent numbers, or they can represent Booleans if someone chooses to use them that way.

In JavaScript, there is the BigInt type to store numbers that are beyond safe integer range. A BigInt number can be created by adding an n character to the end of a number. With BigInt, we can make calculations that have results beyond the safe range of normal numbers. For example, we can write the following expressions and still get the numbers we expect:

const x = 2n ** 55n;  
const y = x + 1n;

For x we get 36028797018963968n and for y we get 36028797018963969n , which is what we expect. BigInts can use the same arithmetic operations as numbers like +, *, -, ** and %. A BigInt behaves like a number when converted to a Boolean with functions, keywords, or operators like Boolean, if , || , &&, !. BigInts cannot be operated in the same expressions as numbers. If we try that, we will get a TypeError.


Strings

Strings are used to represent textual data. Each element of the string has its own position in the string. It’s zero-indexed, so the position of the first character of a string is 0. The length property of the string has the total number of characters of the string.

JavaScript strings are immutable. We cannot modify a string that has been created, but we can still create a new string that contains the originally defined string. We can extract substrings from a string with the substr() function and use the concat() function to concatenate two strings.

We should only present text data with strings. If there are more complex structures needed for your data structure, then they shouldn’t be represented with a string. Instead, they should be objects. This is because it’s easy to make mistakes with strings since we can put in the characters we want. Therefore, mistakes are made easily.


Symbols

Symbols are new to ES2015. It is a unique and immutable identifier. Once you have created it, it cannot be copied. Every time you create a new symbol, it’s a unique one. Symbols are mainly used for unique identifiers in an object. That’s a symbol’s only purpose.

There are some static properties and methods of its own that expose the global symbol registry. It is like a built-in object, but it doesn’t have a constructor, so we can’t write new Symbol to construct a symbol object with the new keyword.

To create new symbols, we can write:

const fooSymbol = Symbol('foo')

Note that each time we call the Symbol function, we get a new symbol, so if we write

Symbol('sym') === Symbol('sym')

the expression above would be false.


Objects

Object is a reference data type, which means it can be referenced by an identifier that points to the location of the object in memory. In memory, the object’s value is stored, and, with the identifier, we can access the value. Object has properties, which are key-value pairs with the values being able to contain the data with primitive types or other objects. That means we can use object to build complex data structures. The key is an identifier for the values of a property, which can be stored as a string or symbol. There are two types of properties that have certain attributes in an object. Objects have data properties and accessor properties.

A JavaScript object has the following data properties:

  • [[Value]] — This can be of any type. It has the value retrieved by a getter of the property. Defaults to undefined.
  • [[Writable]] — This is a Boolean value. If it’s false, then [[Value]] can’t be changed. Defaults to false.
  • [[Enumerable]] — This is a Boolean value. If it’s true, then it can be iterated over by the for...in loop, which is used to iterate over the properties of an object. Defaults to false.
  • [[Configurable]] — This is a Boolean value. If it’s true, then the property can be deleted or changed to an accessor property, and all attributes can be changed. Otherwise, the property can’t be deleted or changed to an accessor property, and attributes other than [[Value]] and [[Writable]] can’t be changed. Defaults to false.

A JavaScript object has the following accessor properties:

  • [[Get]] — This is either a function or it’s undefined. This may contain a function that is used to retrieve a property value whenever a property is being retrieved. Defaults to undefined.
  • [[Set]] — This is either a function or it’s undefined. This lets us set the assigned value to an object’s property whenever an object’s property is attempted to be changed. Defaults to undefined.
  • [[Enumerable]] — This is a Boolean value that defaults to false. If it’s true, then the property will be included when we loop through the properties with the for...in loop.
  • [[Configurable]] — This is a Boolean value that defaults to false. If it’s false, then we can’t delete the property and can’t make changes to it.

JavaScript has a Date object built into the standard library, so we can use it to manipulate dates.

Array are also objects. Array can store a list of data with integer indexes to indicate its position. The first index of JavaScript arrays is 0. There’s also a length property to get the size of the array. The Array object has many convenient methods to manipulate arrays, like the push method to add items to the end of the array and the indexOf method to find the index of the first occurrence of a given value. ATypedArray object is an object that lets us see an array-like view of a binary data buffer.

Since ES2015, the following typed array objects are available:

  • Int8Array, value ranges from -128 to 127
  • Uint8Array, value ranges from 0 to 255
  • Uint8ClampedArray, value ranges from 0 to 255
  • Int16Array, value ranges from -32768 to 32767
  • Uint16Array, value ranges from 0 to 65535
  • Int32Array, value ranges from -2147483648 to 2147483647
  • Unit32Array, value ranges from 0 to 4294967295
  • Floar32Array, value ranges from -1.2 times 10 to the 38 to 3.4times 10 to the 38
  • Float64Array, value ranges from 5.0 times 10 to the 324 to 1.8 times 10 to the 308
  • BigInt64Array, value ranges from -2 to the 63 to 2 to the 63 minus 1
  • BigUint64Array, value ranges from 0 to 2 to the 64 minus 1

Since ES2015, we have new iterable object types. They are Map, Set, WeakMap, WeakSet. Set and WeakSet represent sets of objects, and Map and WeakMap represent objects with a list of key-value pairs. Map keys can be iterated over, butWeakMap’s keys cannot be.


JSON

JSON is a data serialization format that resembles JavaScript objects. They can contain plain data, which means that functions and other kinds of dynamic code aren’t valid in JSON. JSON is frequently used for sending and receiving data for HTTP requests.


The Typeof Operator

With the typeof operator, we can determine the type of a variable or a given value.


JavaScript data types are for classifying data stored in memory into different types. We have numbers and BigInts to store numbers large and small. Also, we have strings to store data. To store more complex data, we can use objects to store things like lists in array objects and maps for key-value pairs. Objects themselves also consist of key-value pairs. In recent versions of JavaScript, we also have Typed Arrays to store binary data with different ranges of values. To transmit and receive data, we can use JSON. Finally, we can use the typeof operator to determine the data type of your variable or value.