Categories
JavaScript Basics

Use Modules to Build a Modular JavaScript App

One of the big features of ES6 is JavaScript supporting built-in modules. Modules allow us to share code between files by using the export and import syntax. This is a big improvement over using script tags and global variables to share code across files.

Using script tags was error prone since the load order matters. Having scripts in the wrong order could cause our program to execute code that hasn’t been declared yet. It also forced us to write spaghetti code with no real structure or logical composition. This problem doesn’t exist with modules because everything is exported and imported directly between files. Also, we can know the definition of the imported code easily since it’s explicit in which modules is being imported and referenced.

Exports and Imports

To make code in a JavaScript file importable, we have to export them explicitly with the export statement. To do this, we just put export in front of the variable or constant you want to expose to other files. For example, we can write:

export let num = 1;

This exports the num variable so that other modules can import it and use it.

We can export anything declared with var, let, const, and also functions and classes. The items exported must be declared at the top level. export cannot be used anywhere else, like inside functions and classes.

We can export multiple members at once. All we have to do is wrap all the members in curly brackets separated by commas. For example, we can write:

const num1 = 1;
const num2 = 2;
const num3 = 3;
export {num1, num2, num3};

This will let us import num1, num2, and num3 in other JavaScript files.

Now that we have exported the members, we can import them in other JavaScript files. We can use the import statement to import one or more members into a module and work with them. For example, if we have the following in moduleA.js:

const num1 = 1;
const num2 = 2;
const num3 = 3;
export {num1, num2, num3};

Then in moduleB.js we can write the following code to import the items from moduleA.js:

import {num1, num2, num3} from './moduleA'

The path after the from keyword starts with a period. The period means that we’re in the current folder.

This assumes that moduleA.js and moduleB.js are in the same folder. If we have them in different folder, then we have the specify the path of moduleA.js relative to moduleB.js if we want to import the exported members of moduleA.js into moduleB.js. For example, if moduleA.js is one level above moduleB.js, then in moduleB.js we write:

import {num1, num2, num3} from '../moduleAFolder/moduleA'

The 2 periods before the path means that we go up one folder level and then get the moduleAFolder and then get moduleA.js.

We can also use JavaScript modules in script tags. To do this, we must set the type attribute of the script tag to module to use them. For example, if we want to use moduleA.js in our HTML file, we can write:

<script type='module' src='moduleA.js'></script>

We can use import and export in JavaScript modules. They won’t work with regular scripts.

Scripts run in strict mode automatically, so we can’t accidentally declare global variables and do other things that can be done without strict mode being enabled. They also load asynchronously automatically so that we won’t have to worry about long scripts holding up a page from loading. Also, import and export only happens between 2 scripts, so no global variables are set. Therefore, they can’t be viewed in the console directly.

Default Exports

There’s also a default export option for exporting a module’s members. We previously exported the variable in a way where we import them by the name. There’s also the default export which exports a single member from a module without needing to reference it explicitly by name when you’re importing it. For example, if we have a single member in a module that we want to export, we can write the following in moduleA.js:

const num = 1;
export default num;

There are no curly braces when you use export default .

Then in the file where you want to import the member. In moduleB.js we write:

import num from './moduleA'

Once again, we omit the curly braces. This is because only one default export is allowed per module. Alternatively, we can write the following in moduleB.js :

import {default as num} from './moduleA'

Renaming Imports and Exports

If we have many modules and their exported members have the same name, then there will be conflicts if we try to import multiple modules. This is will be a problem that we need to fix. Fortunately, JavaScript has the as keyword to let us rename exports and imports so we can avoid name conflicts in our code. To use the as keyword to rename exports, we write the following in moduleA.js:

export {
  num1 as numOne,
  num2 as numTwo
}

Then in moduleB.js, we can import them by writing:

import { numOne, numTwo } from './`moduleA'`

Alternatively, we can do the renaming when we import instead. To do this, in moduleA.js, we put:

export {
  num1,
  num2
}

Then in moduleB.js, we put:

import { num1 as numOne, num2 as numTwo } from './`moduleA'`

If we try to import members with modules where the members have the same name, like:

`import {` num1, num2 `} from './moduleA';
import {` num1, num2 `} from './moduleB';
import {` num1, num2 `} from './moduleC';`

We will see that we get SyntaxError. So we have to rename them so that the module will run:

`import {` num1 as num1A, num2 `as num2A } from './moduleA';
import {` num1 as num1B, num2 `as num2B } from './moduleB';
import {` num1 as num1C, num2 `as num2C } from './moduleC';`

A cleaner way to import from multiple modules that have members with the same names is to import all of the module’s exported members as one object. We can do that with an asterisk. For example, instead of:

`import {` num1 as num1A, num2 `as num2A } from './moduleA';
import {` num1 as num1B, num2 `as num2B } from './moduleB';
import {` num1 as num1C, num2 `as num2C } from './moduleC';`

We can instead write:

import * as moduleA from './moduleA';
import * as moduleB from './moduleB';
import * as moduleC from './moduleC';

Then in code below the imports, we can write:

moduleA.num1;
moduleA.num2;
moduleB.num1;
moduleB.num2;
moduleC.num1;
moduleC.num2;

We can also export and import classes. So if we have a file that contains one or more classes, like the Person.js file with the class below:

class Person {
  constructor(firstName, lastName) {
    this._firstName = firstName;
    this._lastName = lastName;
  }
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  }
  get firstName() {
    return this._firstName
  }
  get lastName() {
    return this._lastName
  }
  sayHi() {
    return `Hi, ${this.firstName} ${this.lastName}`
  }
  set firstName(firstName) {
    this._firstName = firstName;
  }
  set lastName(lastName) {
    this._lastName = lastName;
  }
}

Then we write the following to export a class:

export { Person };

This exports the Person class, and then to import it, we write:

import { Person } from './person';

Dynamic Module Loading

JavaScript modules can be loaded dynamically. This lets us only load modules when they’re needed rather than loading all of them when the app runs. To do this, we use the import() function, which returns a promise. When the module in the argument is loaded, then the promise is fulfilled. The promise resolves to a module object, which can then be used by the app’s code. If we have the Person class in Person.js, then we can dynamically import it with the following code:

import('./Person')
.then((module)=>{
  const Person = module.Person;
  const person = new Person('Jane', 'Smith');
  person.sayHi();
})

Or using the async and await syntax, we can put that in a function:

const importPerson = async ()=>{
  const module = await import('./Person');
  const Person = module.Person;
  const person = new Person('Jane', 'Smith');
  person.sayHi();
}

importPerson();

As you can see, JavaScript modules are very useful for organizing code. It allows us to export things that we want to expose to other modules, eliminating the need for global variables. In addition, exports and imports can be renamed to avoid conflict when importing multiple modules. Also, all the exported members can be imported all at once so that we get the whole module as an object instead of importing individual members. Finally, we can use export default if we only want to export one thing from our module.

Categories
JavaScript Basics

Handling Exceptions in JavaScript

Like any programs, JavaScript will encounter error situations, for example, like when JSON fails to parse, or null value is encounter unexpectedly in a variable. This means that we have to handle those errors gracefully if we want our app to give users a good user experience. This means that we have to handle those errors gracefully. Errors often come in the form of exceptions, so we have to handle those gracefully. To handle them, we have to use the try...catch statement to handle these errors so they do not crash the program.

Try…Catch

To use the try...catch block, we have to use the following syntax:

try{
  // code that we run that may raise exceptions
  // one or more lines is required in this block
}
catch (error){
  // handle error here
  // optional if finally block is present
}
finally {
  // optional code that run either
  // when try or catch block is finished
}

For example, we can write the following code to catch exceptions:

try {
  undefined.prop
} catch (error) {
  console.log(error);
}

In the code above, we were trying to get a property from undefined , which is obviously not allowed, so an exception is thrown. In the catch block, we catch the ‘TypeError: Cannot read property ‘prop’ of undefined’ that’s caused by running undefined.prop and log the output of the exception. So we get the error message outputted instead of crashing the program.

The try...catch statement has a try block. The try block must have at least one statement inside and curly braces must always be used, event for single statements. Then either the catch clause or finally clause can be included. This means that we can have:

try {
  ...
}
catch {
  ...
}

try {
  ...
}
finally{
  ...
}

try {
  ...
}
catch {
  ...
}
finally {
  ...
}

The catch clause has the code that specifies what to do when an exception is thrown in the try block. If they try block didn’t succeed and an exception is thrown, then the code in the catch block will be ran. If all the code in the try block is ran without any exception thrown, then the code in the catch block is skipped.

The finally block executes after all the code the try block or the catch block finishes running. It always runs regardless if exceptions are thrown or not.

try blocks can be nested within each other. If the inner try block didn’t catch the exception and the outer one has a catch block, then the outer one will catch the exception thrown in the inner try block. For example, if we have:

try {
  try {
    undefined.prop
  } finally {
    console.log('Inner finally block runs');
  }
} catch (error) {
  console.log('Outer catch block caught:', error);
}

If we run the code above, we should see ‘Inner finally block runs’ and ‘Outer catch block caught: TypeError: Cannot read property ‘prop’ of undefined’ logged, which is what we expect since the inner try block didn’t catch the exception with a catch block so the outer catch block did. As we can see the inner finally block ran before the outer catch block. try...catch...finally runs sequentially, so the code that’s added earlier will run before the ones that are added later.

The catch block that we wrote so far are all unconditional. That means that they catch any exceptions that were thrown. The error object holds the data about the exception thrown. It only holds the data inside the catch block. If we want to keep the data outside it then we have to assign it to a variable outside the catch block. After the catch block finishes running, the error object is no longer available.

The finally clause contains statements that are excepted after the code in the try block or the catch block executes, but before the statements executed below the try...catch...finally block. It’s executed regardless whether an exception was thrown. If an exception is thrown, then statements in the finally block is executed even if no catch block catches and handles the exception.

Therefore, the finally block is handy for making our program fail gracefully when an error occurs. For example, we can put cleanup code that runs no matter is an exception is thrown or not, like for close file reading handles. The remaining code in a try block doesn’t executed when an exception is thrown when running a line in the try block, so if we were excepted to close file handles in the try and an exception is thrown before the line that closes the file handle is ran, then to end the program gracefully, we should do that in the finally block instead to make sure that file handles always get cleaned up. We can just put code that runs regardless of whether an exception is thrown like cleanup code in the finally block so that we don’t have to duplicate them in the try and catch blocks. For example, we can write:

openFile();
try {
  // tie up a resource
  writeFile(data);
}
finally {
  closeFile();
  // always close the resource
}

In the code above, the closeFile function always run regardless of whether an exception is thrown when the writeFile is run, eliminating duplicate code.

We can have nested try blocks, like in the following code:

try {
  try {
    throw new Error('error');
  }
  finally {
    console.log('finally runs');
  }
}
catch (ex) {
  console.error('exception caught', ex.message);
}

If we look at the console log, we should see that ‘finally runs’ comes before ‘exception caught error.’ This is because everything in the try...catch block is ran line by line even if it’s nested. If we have more nesting like in the following code:

try {
  try {
    throw new Error('error');
  }
  finally {
    console.log('first finally runs');
  }

 try {
    throw new Error('error2');
  }
  finally {
    console.log('second finally runs');
  }
}
catch (ex) {
  console.error('exception caught', ex.message);
}

We see that we get the same console log output as before. This is because the first inner try block didn’t catch the exception, so the exception is propagated to and caught by the outer catch block. If we want to second try block to run, then we have to add a catch block to the first try block, like in the following example:

try {
  try {
    throw new Error('error');
  }
  catch {
    console.log('first catch block runs');
  }
  finally {
    console.log('first finally runs');
  }

 try {
    throw new Error('error2');
  }
  finally {
    console.log('second finally runs');
  }
}
catch (ex) {
  console.error('exception caught', ex.message);
}

Now we see the following message logged in order: ‘first catch block runs’, ‘first finally runs’, ‘second finally runs’, ‘exception caught error2’. This is because the first try block has a catch block, so the the exception caused by the throw new Error('error') line is now caught in the catch block of the first inner try block. Now the second inner try block don’t have an associated catch block, so error2 will be caught by the outer catch block.

We can also rethrow errors that were caught in the catch block. For example, we can write the following code to do that:

try {
  try {
    throw new Error('error');
  } catch (error) {
    console.error('error', error.message);
    throw error;
  } finally {
    console.log('finally block is run');
  }
} catch (error) {
  console.error('outer catch block caught', error.message);
}

As we can see, if we ran the code above, then we get the following logged in order: ‘error error’, ‘finally block is run’, and ‘outer catch block caught error’. This is because the inner catch block logged the exception thrown by throw new Error(‘error’) , but then after console.error(‘error’, error.message); is ran, we ran throw error; to throw the exception again. Then the inner finally block is run and then the rethrown exception is caught by the outer catch block which logged the error that was rethrown by the throw error statement in the inner catch block.

Since the code runs sequentially, we can run return statements at the end of a try block. For example, if we want to parse a JSON string into an object we we want to return an empty object if there’s an error parsing the string passed in, for example, when the string passed in isn’t a valid JSON string, then we can write the following code:

const parseJSON = (str) => {
  try {
    return JSON.parse(str);
  }
  catch {
    return {};
  }
}

In the code above, we run JSON.parse to parse the string and if it’s not valid JSON, then an exception will be thrown. If an exception is thrown, then the catch clause will be invokes to return an empty object. If JSON.parse successfully runs then the parsed JSON object will be returned. So if we run:

console.log(parseJSON(undefined));
console.log(parseJSON('{"a": 1}'))

Then we get an empty object on the first line and we get {a: 1} on the second line.

Try Block in Asynchronous Code

With async and await , we can shorten promise code. Before async and await, we have to use the then function, we make to put callback functions as an argument of all of our then functions. This makes the code long is we have lots of promises. Instead, we can use the async and await syntax to replace the then and its associated callbacks as follows. Using the async and await syntax for chaining promises, we can also use try and catch blocks to catch rejected promises and handle rejected promises gracefully. For example , if we want to catch promise rejections with a catch block, we can do the following:

(async () => {
  try {
    await new Promise((resolve, reject) => {
      reject('error')
    })
  } catch (error) {
    console.log(error);
  }

})();

In the code above, since we rejected the promise that we defined in the try block, the catch block caught the promise rejection and logged the error. So we should see ‘error’ logged when we run the code above. Even though it looks a regular try...catch block, it’s not, since this is an async function. An async function only returns promises, so we can’t return anything other than promises in the try...catch block. The catch block in an async function is just a shorthand for the catch function which is chained to the then function. So the code above is actually the same as:

(() => {
  new Promise((resolve, reject) => {
      reject('error')
    })
    .catch(error => console.log(error))
})()

We see that we get the same console log output as the async function above when it’s run.

The finally block also works with the try...catch block in an async function. For example, we can write:

(async () => {
  try {
    await new Promise((resolve, reject) => {
      reject('error')
    })
  } catch (error) {
    console.log(error);
  } finally {
    console.log('finally is run');
  }
})();

In the code above, since we rejected the promise that we defined in the try block, the catch block caught the promise rejection and logged the error. So we should see ‘error’ logged when we run the code above. The the finally block runs so that we get ‘finally is run’ logged. The finally block in an async function is the same as chaining the finally function to the end of a promise so the code above is equivalent to:

(() => {
  new Promise((resolve, reject) => {
      reject('error')
    })
    .catch(error => console.log(error))
    .finally(() => console.log('finally is run'))
})()

We see that we get the same console log output as the async function above when it’s run.

The rules for nested try...catch we mentioned above still applies to async function, so we can write something like:

(async () => {
  try {
    await new Promise((resolve, reject) => {
      reject('outer error')
    })
    try {
      await new Promise((resolve, reject) => {
        reject('inner error')
      })
    } catch (error) {
      console.log(error);
    } finally {

    }
  } catch (error) {
    console.log(error);
  } finally {
    console.log('finally is run');
  }
})();

This lets us easily nest promises and and handle their errors accordingly. This is cleaner than chaining the then , catch and finally functions that we did before we have async functions.

To handle errors in JavaScript programs, we can use the try...catch...finally blocks to catch errors. This can be done with synchronous or asynchronous code. We put the code that may throw exceptions in the try block, then put the code that handles the exceptions in the catch block. In the finally block we run any code that runs regardless of whether an exception is thrown. async functions can also use the try...catch block, but they only return promises like any other async function, but try...catch...finally blocks in normal functions can return anything.

Categories
JavaScript JavaScript Basics

Using JavaScript BigInt to Represent Large Numbers

To represent integers larger than 2**53 – 1 in JavaScript, we can use the BigInt object to represent the values.

It can be manipulated via normal operations like arithmetic operators — addition, subtraction, multiplication, division, remainder, and exponentiation.

It can be constructed from numbers and hexadecimal or binary strings. Also, it supports bitwise operations like AND, OR, NOT, and XOR. The only bitwise operation that doesn’t work is the zero-fill right-shift operator (>>> ) because BigInts are all signed.

Also, the unary + operator isn’t supported to not break asm.js. These operations are only done when all the operands are BigInts. We can’t have some operands being BigInts and some being numbers.

In JavaScript, a BigInt is not the same as a normal number. It’s distinguished from a normal number by having an n at the end of the number. We can define a BigInt with the BigInt factory function.

It takes one argument that can be an integer number or a string representing a decimal integer, hexadecimal string, or binary string. BigInt cannot be used with the built-in Math object.

Also, when converting between number and BigInt and vice versa, we have to be careful because the precision of BigInt might be lost when a BigInt is converted into a number.


Usage

To define a BigInt, we can write the following if we want to pass in a whole number:

const bigInt = BigInt(1);  
console.log(bigInt);

It would log 1n when we run the console.log statement. If we want to pass a string into the factory function instead, we can write:

const bigInt = BigInt('2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222');  
console.log(bigInt);

We can also pass an hexadecimal number string with a string that starts with 0x into the factory function:

const bigHex = BigInt("0x1fffffffffffff111111111");  
console.log(bigHex);

The code above would log 618970019642690073311383825n when we run console.log on bigHex.

Likewise, we can pass in a binary string with a string that starts with 0b and binary string in the remainder of the string to get a BigInt:

const bigBin = BigInt("0b111111111111111000000000011111111111111111111111");  
console.log(bigBin);

The code above would get us 281466395164671n when we run console.log on it. Passing in strings would be handy if the BigInt that we want to create is outside of the range that can be accepted by the number type.

We can also define a BigInt with a BigInt literal, we can just attach an n character to the whole number. For example, we can write:

const bigInt = 22222222222222222222222222222222n;  
console.log(bigInt);

Then, we get 22222222222222222222222222222222n as the value of bigInt when we log the value.

BigInt has its own data type in JavaScript. When you run the typeof operator on a BigInt variable, constant or value, we would get bigint. For example, when we run:

typeof 2n === 'bigint';  
typeof BigInt(2) === 'bigint';

Both lines of code would evaluate to true. However, if we wrap it with the Object factory function, we get that the type is an object:

typeof Object(2n) === 'object';

The code above would evaluate to true.

We can apply number operations to BigInts.

This includes the arithmetic operators — addition, subtraction, multiplication, division, remainder, and exponentiation. Also, it supports bitwise operations like AND, OR, NOT, and XOR.

The only bitwise operation that doesn’t work is the zero-fill right-shift operator (>>> ) because BigInts are all signed. Also, the unary + operator isn’t supported to not break asm.js.

These operations are only done when all the operands are BigInts. We can’t have some operands being BigInts and some being numbers. For example, if we have:

const bigInt = BigInt(Number.MAX_SAFE_INTEGER);  
console.log(bigInt);
const biggerInt = bigInt + BigInt(1);  
console.log(biggerInt);
const evenBiggerInt = bigInt + BigInt(2);  
console.log(evenBiggerInt);
const multi = bigInt * BigInt(2);  
console.log(multi);
const subtr = bigInt - BigInt(10);  
console.log(subtr);
const remainder = bigInt % BigInt(1);  
console.log(remainder);
const bigN = bigInt ** BigInt(54);  
console.log(bigN);
const veryNegativeNum = bigN * -BigInt(1)  
console.log(veryNegativeNum);

We get 9007199254740991n for bigInt, 9007199254740992n for biggerInt, 9007199254740993n for evenBiggerInt, 18014398509481982n for multi, 9007199254740981n for subtr, 0n for remainder, 3530592467533273200243077170885155617107348521747142286627863260349958518655132050034081285541183004983189865471543609006121689601641796259277395721973496380268998810860889580999688899063966604079229944616948651888866122410855207004436640389001057851295873774080462273415460559916461808220601907673652198075821210257903676343961872269549414664419834643799298966710366275919846143068708381391506113181640387818197335712192797007703730122048543818655729529755334964590919971124632271934272078761071878238334159341746985273963326734748343552398522547662400805644304911487571159654254814460707275228515191584712593238883953404971043549757327554636466197405269908317698331974392008288867249576664013677011521696874812515379689360830743272800013459321098384864332719963035293422648481458040217707301509007592199565531403472471705983351384755965631442881685949576642561n for bigN, and -3530592467533273200243077170885155617107348521747142286627863260349958518655132050034081285541183004983189865471543609006121689601641796259277395721973496380268998810860889580999688899063966604079229944616948651888866122410855207004436640389001057851295873774080462273415460559916461808220601907673652198075821210257903676343961872269549414664419834643799298966710366275919846143068708381391506113181640387818197335712192797007703730122048543818655729529755334964590919971124632271934272078761071878238334159341746985273963326734748343552398522547662400805644304911487571159654254814460707275228515191584712593238883953404971043549757327554636466197405269908317698331974392008288867249576664013677011521696874812515379689360830743272800013459321098384864332719963035293422648481458040217707301509007592199565531403472471705983351384755965631442881685949576642561n for veryNegativeNum.

Note that when we get fractional results, the fractional parts will be truncated when result has it. BigInt is a big integer and it’s not for storing decimals. For example, in the examples below:

const expected = 8n / 2n;  
console.log(expected)const rounded = 9n / 2n;  
console.log(rounded)

We get 4n for expected and rounded. This is because the fractional part is removed from the BigInt.

Comparison operators can be applied to BigInts. The operands can be either BigInt or numbers. We can use the bigger than, bigger than or equal to, less than, less than or equal to, double equals, and triple equals operators with BigInts.

For example, we can compare 1n to 1:

1n === 1

The code above would evaluate to false because BigInt and numbers aren’t the same types. But when we replace triple equals with double equals, like in the code below:

1n == 1

The statement above evaluates to true because only the value is compared.

Note that in both examples, we mixed BigInt operands with number operands. This is allowed for comparison operators.

BigInts and numbers can be compared together with other operators as well, like in the following examples:

1n < 9  
// true9n > 1  
// true9 > 9n  
// false  
  
9n > 9  
// false  
  
9n >= 9  
// true

Also, they can be mixed together in one array and sorted together. For example, if we have the following code:

const mixedNums = [5n, 6, -120n, 12, 24, 0, 0n];  
mixedNums.sort();  
console.log(mixedNums)

We get [-120n, 0, 0n, 12, 24, 5n, 6]. We also sort it in descending order with the following code:

const mixedNums = [5n, 6, -120n, 12, 24, 0, 0n];  
mixedNums.sort((a, b) => {  
  if (a > b) {  
    return -1  
  } else if (a < b) {  
    return 1  
  }  
  return 0  
});  
console.log(mixedNums)

In the code above, we used the comparison operators to compare the value of the numbers and return -1 if the first number is bigger than the second number, return 1 if the first number is less than the second number, and return 0 otherwise.

If we wrap BigInts with Object, then they’re compared as objects, so two objects are only considered the same if the same instance is referenced. For example, if we have:

0n === Object(0n);  
Object(0n) === Object(0n);  
const o = Object(0n);  
o === o;

Then the first three lines in the code above would evaluate to false while the last line would evaluate to true.

When BigInts are coerced into booleans, they act the same as if they’re numbers. For example, Boolean(0n) would return false, and anything else would return true.

For example, we can coerce them into booleans like in the following code:

0n || 11n  
// 11n  
  
0n && 11n  
// 0n  
  
Boolean(0n)  
// false  
  
Boolean(11n)  
// true  
  
!11n  
// false  
  
!0n  
// true

BigInt Methods

The BigInt object has several methods. There are the static asIntN() and asUintN() methods, and the toLocaleString(), toString(), and valueOf() instance methods.

The asIntN method wraps a BigInt value between -2 to the width minus 1 and 2 to the width minus 1. For example, if we have:

const bigNum = 2n ** (62n - 1n) - 1n;  
console.log(BigInt.asIntN(62, bigNum));

Then we get the literal of the actual value of bigNum modulo 62 returned, which is 2305843009213693951n. However, if we have:

const bigNum = 2n ** (63n - 1n) - 1n;  
console.log(BigInt.asIntN(62, bigNum));

We get -1n since the 2n ** (63n — 1n) modulo 2n ** 63n is returned.

The asUintN() method wraps a BigInt value between 0 and 2 to the width minus 1. For example, if we have:

const bigNum = 2n ** 11n - 1n;  
console.log(BigInt.asUintN(11, bigNum));

Then we get the literal of the actual value of bigNum returned, which is 2305843009213693951n. However, if we have:

const bigNum = 2n ** 11n - 1n;  
console.log(BigInt.asUintN(11, bigNum));

We get 2047n since the 2n ** 11n — 1n modulo 2n ** 11n is returned.

BigInt has a toLocaleString() method to return the value of the string for the BigInt depending on the locale we pass in. For example, if we want to get the French representation of a BigInt, we can write:

const bigNum = 2n ** 60n - 1n;  
console.log(bigNum.toLocaleString('fr'));

The code above will log 1 152 921 504 606 846 975.

The toString() method will convert a BigInt to a string. For example, we can write:

const bigNum = 2n ** 60n - 1n;  
console.log(bigNum.toString());

The code above will log 1152921504606846975.

The valueOf method will get the value of the BigInt object. For example, if we run:

const bigNum = 2n ** 60n - 1n;  
console.log(bigNum.valueOf());

Then we get 1152921504606846975n logged.

BigInts aren’t supported by JSON, so a TypeError would be raised if we try to convert it to JSON with JSON.stringify().

However, we can convert it to something supported like a string, then it can be stored as JSON. We can override the toJSON method of a BigInt by writing:

BigInt.prototype.toJSON = function() {  
  return this.toString();  
}

const bigIntString = JSON.stringify(88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888n)  
console.log(bigIntString);

Then, we get: “88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888”.

BigInts are useful for representing integers larger than 2 to the 53rd power minus 1 in JavaScript, we can use the BigInt object to represent the values.

It can be manipulated via normal operations like arithmetic operators — addition, subtraction, multiplication, division, remainder, and exponentiation.

BigInts support most bitwise operations like AND, OR, NOT, and XOR.

They can also be converted from hexadecimal or binary numbers.

These operations are only done when all the operands are BigInts. We can’t have some operands being BigInts and some being numbers.

In JavaScript, a BigInt is not the same as a normal number. It’s distinguished from a normal number by having an n at the end of the number. We can define a BigInt with the BigInt factory function.

It takes one argument that can be an integer number or a string representing a decimal integer, hexadecimal string, or binary string. BigInt cannot be used with the built-in Math object.

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.