In Pt.3 we implemented the conversion from Hindu-Arabic numbers to Roman numbers. In this final chapter, we will complete our library by adding the conversion from Roman numbers to Hindu-Arabic numbers.
Convert the first five Roman numbers
We will set up the test cases the way we did for the other conversions: we will check both the method toInt() and the method toString(). We will test the first five Roman numbers: ‘I’. ‘II’. ‘III’. ‘IV’ and ‘V’:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
it('The Roman number "I" should be equal to the arabic number 1', () => {
let romannum = RomanNumber('I');
romannum.toString().should.equal('I');
romannum.toInt().should.equal(1);
});
it('The Roman number "II" should be equal to the arabic number 2', () => {
let romannum = RomanNumber('II');
romannum.toString().should.equal('II');
romannum.toInt().should.equal(2);
});
it('The Roman number "III" should be equal to the arabic number 3', () => {
let romannum = RomanNumber('III');
romannum.toString().should.equal('III');
romannum.toInt().should.equal(3);
});
it('The Roman number "IV" should be equal to the arabic number 4', () => {
let romannum = RomanNumber('IV');
romannum.toString().should.equal('IV');
romannum.toInt().should.equal(4);
});
it('The Roman number "V" should be equal to the arabic number 5', () => {
let romannum = RomanNumber('V');
romannum.toString().should.equal('V');
romannum.toInt().should.equal(5);
});
To pass these very first conversion tests, we just need to update the library constructor in this way:
In the code block relative to checkOnlyRomanSymbols we used a Map structure, where we matched Roman numbers (as keys) to Hindu-Arabic numbers (as values).
All the tests passed correctly as shown below:
Convert Roman numbers composed of simple patterns
The next tests we are going to perform, are about ‘simple’ Roman numbers: for simple, we mean those numbers represented by the following patterns:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Patterns:
*
* | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
* Ones: | I | II | III | IV | V | VI | VII | VIII | IX |
*
* | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 |
* Tens: | X | XX | XXX | XL | L | LX | LXX | LXXX | XC |
// this loop is used to read the entire Roman string
while(i < val.length) {
let tmpPattern = '';
// this loop is used to build the next pattern
while(i < val.length) {
if(tmpPattern.length == 0) {
tmpPattern += val[i++];
}
else {
let tmpPatternVal = patterns.get(tmpPattern + val[i]);
if(typeof(tmpPatternVal) !== 'undefined') {
tmpPattern += val[i++];
}
else {
break;
}
}
}
if(tmpPattern.length > 0) {
let tmpNum = patterns.get(tmpPattern);
finalInt += tmpNum;
}
}
return finalInt;
};
The first while loop represents a way to read all the string’s symbols, and the second one will be used to look for a single pattern. Any time a pattern is found, we’ll add the relative integer value to the finalInt.
Let’s finally check the tests: $ npm test
…and here we go!
Make our library more resilient
Now that our Roman numbers library is able to correctly convert any valid Roman number, we just want to make it better by adding the last checks for invalid cases:
No more that three equal consecutive symbols can be found;
A pattern relative to a certain set (e.g. hundreds) cannot be found after a higher or equal set (e.g. thousands or hundreds): for example the invalid Roman number “MIM” contains the pattern “M”, belonging to thousands, after “I”, belonging to ones.
Here are the very last test cases to be added inside Check exceptions:
1
2
3
4
5
6
7
it('The constructor invoked with "IIII" should return an exception: invalid value', () => {
In this third part we will start converting Hindu-Arabic numbers to Roman numbers.
Convert numbers from 1 to 5
Let’s start by creating a brand new describe section in our test file:
1
2
describe('Check values', () => {
});
We will check the first 5 numbers: 1, 2, 3, 4 and 5.
For any number we want to check both the value returned by toInt (Hindu-Arabic number) and the one returned by toString (Roman Number). For number 1, we will test also its string form ‘1’. Let’s insert our six test cases inside our new describe section:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
describe('Check values', () => {
it('The arabic number 1 should be equal to the Roman number "I"', () => {
let romannum = RomanNumber(1);
romannum.toString().should.equal('I');
romannum.toInt().should.equal(1);
});
it('The arabic number "1" should be equal to the Roman number "I"', () => {
let romannum = RomanNumber('1');
romannum.toString().should.equal('I');
romannum.toInt().should.equal(1);
});
it('The arabic number 2 should be equal to the Roman number "II"', () => {
let romannum = RomanNumber(2);
romannum.toString().should.equal('II');
romannum.toInt().should.equal(2);
});
it('The arabic number 3 should be equal to the Roman number "III"', () => {
let romannum = RomanNumber(3);
romannum.toString().should.equal('III');
romannum.toInt().should.equal(3);
});
it('The arabic number 4 should be equal to the Roman number "IV"', () => {
let romannum = RomanNumber(4);
romannum.toString().should.equal('IV');
romannum.toInt().should.equal(4);
});
it('The arabic number 5 should be equal to the Roman number "V"', () => {
let romannum = RomanNumber(5);
romannum.toString().should.equal('V');
romannum.toInt().should.equal(5);
});
});
We need a big refactor in our code. We need to add two static methods to check:
if a Hindu-Arabic number is valid => isValidInt
if a string contains only Roman symbols => checkOnlyRomanSymbols
This function computes ones and tens values from the input, and then it handles them separately to get the appropriate Roman symbols (See how the case 0 is never handled because we have no Roman symbol representing it).
Now let’s just replace the isValidInt code block in the library constructor
Now let’s check what we accomplished so far, by typing: $ npm test
Everything works :)
Refactor intToRoman
Even though our intToRoman method performs correctly, it really needs refactoring (for style’s sake).
The idea is to group the Roman symbols, based on the ones used for ones, tens, hundreds and thousands. In fact, the pattern used is always the same: it’s just the symbols that change.
Here is the final version of our conversion method intToRoman:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/**
* static method intToRoman
* @param {Integer} val: must be an integer between 1 and 3999 (even in the form '1' to '3999')
*
* The patterns for ones, tens, hundreds and thousands are the same:
* only sumbols change:
*
* Patterns:
*
* | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
* Ones: | I | II | III | IV | V | VI | VII | VIII | IX |
*
* | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 |
* Tens: | X | XX | XXX | XL | L | LX | LXX | LXXX | XC |
The second requirement: invoke library as a function (without new)
Let’s create the test for this requirement by adding this code to our testRomanNumber.js:
1
2
3
4
5
6
...
it('The constructor should be callable without new', () => {
let romanNumber = RomanNumber(1);
assert.isObject(romanNumber);
});
...
If we run this new test by typing: $ npm test
we’ll see this error:
In order to pass this test we may use the property new.target added in ECMAScript 2015 (ES6)
The new.target property lets you detect whether a function or constructor was called using the new operator. In constructors and functions instantiated with the new operator, new.target returns a reference to the constructor or function. In normal function calls, new.target is undefined.
Let’s add this piece of code inside RomanNumber constructor
1
2
3
if(!new.target) {
returnnew RomanNumber(val);
}
Then we run the test $ npm test
And, as if by magic, the error disappears:
The third requirement: add toInt and toString() methods
Add two test cases for this requirement, inside the describe “Check exceptions”
1
2
3
4
5
6
7
8
9
it('The object should contain method toInt', () => {
let romanNumber = RomanNumber(1);
assert.isFunction(romanNumber.toInt);
});
it('The object should contain method toString', () => {
let romanNumber = RomanNumber(1);
assert.isFunction(romanNumber.toString);
});
These test cases are bases on the isFunction method of Chai assert interface
If we run the test with: $ npm test
we’ll see that only the toInt() method does not exist:
This is because in Javascript every object has a toString() method
So, in order to pass the test, we just have to add this code to RomanNumber.js:
1
2
3
4
5
6
/**
* toInt
*/
romanNumber.prototype.toInt = functiontoInt() {
return1;
};
Test the correct result by yourself, by typing: $ npm test
Requirement: value passed not null or empty
We have to create three test cases to check null value, empty value, and no value.
1
2
3
4
5
6
7
8
9
10
11
it('The constructor invoked with null should return an exception: value required', () => {
Test-Driven Development is a software development process that relies on the repetition of a very short development cycle: Requirements are turned into very specific test cases, then the software is improved to pass the new tests.
Add a test
Run all tests and see if the new test fails
Write the code
Run tests
Refactor code
Repeat
Thereby the cornerstone of this process is represented by the Unit Test.
Mocha and Chai
In Node.js a wide used test framework is Mocha: it makes synchronous and asynchronous testing simple and straightforward, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases.
The assertion library we will use is Chai: it has several interfaces that allow the developer to choose the most comfortable (should, expect, assert).
Roman Numbers
A Roman number represents an integer (Hindu-Arabic numerals) using a small set of symbols:
Roman Numeral
Hindu-Arabic Equivalent
I
1
V
5
X
10
L
50
C
100
D
500
M
1000
There are a few rules for writing numbers with Roman numerals.
Repeating a numeral up to three times represents addition of the number. For example, III represents 1 + 1 + 1 = 3. Only I, X, C, and M can be repeated; V, L, and D cannot be, and there is no need to do so.
Writing numerals that decrease from left to right represents addition of the numbers. For example, LX represents 50 + 10 = 60 and XVI represents 10 + 5 + 1 = 16.
To write a number that otherwise would take repeating of a numeral four or more times, there is a subtraction rule. Writing a smaller numeral to the left of a larger numeral represents subtraction. For example, IV represents 5 - 1 = 4 and IX represents 10 - 1 = 9. To avoid ambiguity, the only pairs of numerals that use this subtraction rule are:
Roman Numeral
Hindu-Arabic Equivalent
IV
4 = 5 - 1
IX
9 = 10 - 1
XL
40 = 50 - 10
XC
90 = 100 - 10
CD
400 = 500 - 100
CM
900 = 1000 - 100
Following these rules, every Hindu-Arabic number between 1 and 3999 (MMMCMXCIX) can be represented as a Roman numeral.
Requirements
The Roman Number Library to build will have to take a value in input (a Roman Number or an Hindu-Arabic number), and will have to return an object containing two methods:
Under the test folder, create the file testRomanNumber.js with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Require the dev-dependencies
let chai = require('chai');
let should = chai.should();
let expect = chai.expect;
let assert = chai.assert;
const RomanNumber = require('../RomanNumber');
describe('RomanNumber', () => {
describe('Check exceptions', () => {
it('The constructor should return an object', () => {
let romanNumber = new RomanNumber(1);
assert.isObject(romanNumber);
});
});
});
In this piece of code we include Chai with its three interfaces (should, expect, assert).
Then we include RomanNumber.js (which is still empty).
“describe“ is used merely for grouping test cases, which you can nest as deep.
“it“ is a test case: here the very first test case is about invoking the library’s constructor let romanNumber = new RomanNumber(1) and check that it returns an object assert.isObject(romanNumber).
To launch the test, just type: $ npm test
We have an error stating that RomanNumber is not a constructor: this was the expected error hence our library code is still empty.
Init Library
In order to pass the test, let’s add this simple piece of code to out Roman Number library (RomanNumber.js):
1
2
3
4
const romanNumber = functionRomanNumber(val) {
};
module.exports = romanNumber;
Now, let’s run the test again: $ npm test
More to come
In Pt.2 we will add more tests based on the requirements we defined, and the code to pass those tests.
In my application named MyPortfolio (built with Node.js/Express and MongoDB), I decided to use Passport as authentication middleware.
It is extremely flexible and modular, and it comes with a comprehensive set of strategies which support authentication using a username and password, Google-OAuth, Facebook, Twitter, and more.
I started developing a Passport authentication, by following a very interesting tutorial of Scotch.io, where it is clearly illustrated how to use Passport to perform local authentication, Facebook, Twitter, Google authentications, and even how to link all social accounts under one user account.
But there was something lacking in that tutorial, and many other Passport tutorials I found on the web: how could I use Passport middleware to perform a sign-up only after user viewed and accepted my application’s Terms of Use?
Let me show you the architecture, the models and the operations flow I used (check also the code here MyPortfolio code).
Package.json
Here are the dependencies I’m using in package.json:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// package.json
...
"dependencies": {
"async": "^2.5.0",
"bcrypt-nodejs": "0.0.3",
"body-parser": "^1.18.2",
"connect-flash": "^0.1.1",
"cookie-parser": "^1.4.3",
"dotenv": "^4.0.0",
"ejs": "^2.5.7",
"express": "^4.14.0",
"express-ejs-layouts": "^2.3.1",
"express-session": "^1.15.6",
"express-validator": "^4.2.1",
"fs-extra": "^4.0.2",
"mongoose": "^4.11.13",
"multer": "^1.3.0",
"passport": "^0.4.0",
"passport-facebook": "^2.1.1",
"passport-google-oauth": "^1.0.0",
"passport-local": "^1.0.0",
"passport-twitter": "^1.0.4",
"summernote-nodejs": "^1.0.4"
}
...
Because of I decided to let users log in only with their Google accounts in MyPortfolio, the dependencies of interest in this post are:
express is the framework.
express-session to handle the session in Express framework.
ejs is the templating engine.
mongoose is the object modeling for our MongoDB database.
passport and passport-google-oauth for Google authentication.
connect-flash allows to pass session flashdata messages.
fs-extra empowers Node.js fs library with Promises.
User model
Here is the user model schema, which resembles the one in the Scotch.io tutorial.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// user.js
...
var userSchema = mongoose.Schema({
local : {
email : String,
password : String,
},
facebook : {
id : String,
token : String,
email : String,
name : String
},
twitter : {
id : String,
token : String,
displayName : String,
username : String
},
google : {
id : String,
token : String,
email : {type: String, index: true},
name : {type: String, index: true},
imageUrl : String,
eslug : String
},
mustacceptterms : {
type: Boolean,
default: 'false'
}
});
...
You can notice that in the userSchema I added another boolean parameter, named mustacceptterms: this one will be true until the user will have explicitly confirmed to have read and accepted the Terms of Use.
Application setup
server.js contains the setup of the different packages, included Express, Passport, Session and Mongoose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// server.js
// load environment variables
require('dotenv').config();
// grab our dependencies
const express = require('express'),
app = express(),
port = process.env.PORT || 8080
expressLayouts = require('express-ejs-layouts'),
mongoose = require('mongoose'),
passport = require('passport'),
bodyParser = require('body-parser'),
session = require('express-session'),
cookieParser = require('cookie-parser'),
flash = require('connect-flash'),
expressValidator = require('express-validator');
// configure our application
// set sessions and cookie parser
app.use(cookieParser());
app.use(session({
secret: process.env.SECRET,
cookie: {maxAge: 8*60*60*1000}, // 8 hours
resave: false, // forces the session to be saved back to the store
saveUninitialized: false// don't save unmodified sessions
// if there is a user id already but no token (user was linked at one point and then removed)
if (!user.google.token) {
user.google.token = token;
user.google.name = profile.displayName;
user.google.email = profile.emails[0].value; // pull the first email
user.google.imageUrl = profile.photos[0].value; // pull the first image
initUserFolderStructure(profile.id, () => {
user.save(function(err) {
if (err) {
throw err;
}
return done(null, user);
});
});
}
if(!user.mustacceptterms) {
initUserFolderStructure(profile.id, () => {
user.save(function(err) {
if (err) {
throw err;
}
return done(null, user);
});
});
}
else {
return done(null, user);
}
}
// brand new user
else {
var newUser = new User();
newUser.google.id = profile.id;
newUser.google.token = token;
newUser.google.name = profile.displayName;
newUser.google.email = profile.emails[0].value; // pull the first email
newUser.google.imageUrl = profile.photos[0].value; // pull the first image
newUser.mustacceptterms = true; // the new user must accept terms of use before creating the account on MyPortfolio
newUser.save(function(err) {
if (err) {
throw err;
}
return done(null, newUser);
});
}
});
}
else {
// user already exists and is logged in, we have to link accounts
var user = req.user; // pull the user out of the session
user.google.id = profile.id;
user.google.token = token;
user.google.name = profile.displayName;
user.google.email = profile.emails[0].value; // pull the first email
user.google.imageUrl = profile.photos[0].value; // pull the first image
initUserFolderStructure(profile.id, () => {
user.save(function(err) {
if (err) {
throw err;
}
return done(null, user);
});
});
}
}));
If a user logs in to MyPortfolio for the first time (‘A brand new user’), he will be saved to the database with the flag mustacceptterms set to true, and no folder structure will be created.
Even if the user had previously logged in without accepting the Terms of Use, he would just be considered authenticated, but the flag mustacceptterms would remain set to true.
So I added two different functions to determine if user is really logged-in, or if he’s just authenticated, and the flag mustacceptterms is the determiner.
Handling routing
The routes.js contains the authentication logic (and much more):
The utility function isLoggedIn determines if a user is logged in the application.
The utility function isLoggedInToAcceptTerms determines if a user is just authenticated (but still never accepted the Terms of Use).
The route ‘/auth/google’ uses passport authentication method (which was previously configured with the Google Oauth2 Strategy) to authenticate the user with his Google account.
The magic happens for the route ‘/auth/google/callback’, which represents the callback invoked after the Google authentication procedure. I used a ‘failureRedirect’ to the home page when the authentication fails, and a Custom function on correct Google authentication.
Thanks to this custom function I can determine whether the Terms of Use haven’t been previously accepted, and in that case redirect the user to the Terms of Use page: from there the user will have the chance to read and accept the Terms and complete the sign-in process.