├── cache └── keep.txt ├── data ├── ._1.json ├── ._2.json ├── ._3.json ├── ._invalid.json ├── invalid.json ├── 1.json ├── 2.json └── 3.json ├── tags.txt ├── spec ├── support │ └── jasmine.json ├── loadTagsSpec.js └── tagSearchSpec.js ├── package.json ├── modules ├── sortoutput.js ├── loadtags.js ├── cachemodule.js └── tagsearch.js ├── README.md └── app.js /cache/keep.txt: -------------------------------------------------------------------------------- 1 | 0 -------------------------------------------------------------------------------- /data/._1.json: -------------------------------------------------------------------------------- 1 | Mac OS X 2 | -------------------------------------------------------------------------------- /data/._2.json: -------------------------------------------------------------------------------- 1 | Mac OS X 2 | -------------------------------------------------------------------------------- /data/._3.json: -------------------------------------------------------------------------------- 1 | Mac OS X 2 | -------------------------------------------------------------------------------- /data/._invalid.json: -------------------------------------------------------------------------------- 1 | Mac OS X 2 | -------------------------------------------------------------------------------- /data/invalid.json: -------------------------------------------------------------------------------- 1 | this is not a valid json file 2 | -------------------------------------------------------------------------------- /tags.txt: -------------------------------------------------------------------------------- 1 | lorem 2 | ipsum 3 | dolor 4 | sit 5 | amet 6 | -------------------------------------------------------------------------------- /data/1.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "tags": ["diam", "ipsum", "consectetur"] 4 | } 5 | -------------------------------------------------------------------------------- /data/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Second object", 3 | "tags": ["yolo", "ipsum", "diam", "amet", "lectus"] 4 | } 5 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /data/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": ["dolor"], 3 | "children": [ 4 | { 5 | "foo": "bar", 6 | "tags": ["ipsum", "dolor", "amet"] 7 | }, 8 | { 9 | "baz": "buzzle", 10 | "tags": null 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-exam-completed", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "\u001b[D\u001b[D\u001b[D\u001bapp.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "test": "node ./node_modules/.bin/jasmine-node spec" 9 | }, 10 | "author": "Jonathan Spruance", 11 | "license": "ISC", 12 | "dependencies": { 13 | "jasmine-node": "^1.14.5" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spec/loadTagsSpec.js: -------------------------------------------------------------------------------- 1 | var loadTags = require('../modules/loadtags'); 2 | var inputtags = ""; 3 | 4 | describe("Load Tags", function() { 5 | 6 | // needed for async jasmine support 7 | beforeEach(function(done) { 8 | loadTags(inputtags, function (err, tags) { 9 | done(); 10 | }); 11 | }); 12 | 13 | it("should return an array of tags", function(done) { 14 | expect(tags).not.toBeUndefined(); 15 | expect(tags).toBeTruthy(); 16 | expect(tags).not.toBe(null); 17 | expect(tags.length).toBeGreaterThan(0); 18 | done(); 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /spec/tagSearchSpec.js: -------------------------------------------------------------------------------- 1 | var tagSearch = require('../modules/tagsearch'); 2 | var loadTags = require('../modules/loadtags'); 3 | var inputtags = ""; 4 | 5 | describe("Tag Search", function() { 6 | 7 | // needed for async jasmine support 8 | beforeEach(function(done) { 9 | loadTags(inputtags, function (err, tags) { 10 | 11 | tagSearch(tags, function (err, data) { 12 | 13 | done(); 14 | }); 15 | }); 16 | }); 17 | 18 | it("expect some tag match data to be returned", function(done) { 19 | console.log("dataaaaaaaaaaaaaaaaa" + data + " error :: " + err); 20 | expect(data).not.toBeUndefined(); 21 | expect(data).toBeTruthy(); 22 | expect(data).not.toBe(null); 23 | done(); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /modules/sortoutput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module sortOutput 3 | * @summary Helper module to sort output of tags and match 4 | * counts before printing to console 5 | * 6 | */ 7 | 8 | var sortOutput = function (obj) { 9 | 10 | var sortedOutput = []; 11 | 12 | if( Object.prototype.toString.call(obj) === '[object Array]' ) { 13 | for (var i = 0, len = obj.length; i < len; i++) { 14 | // push data to array for sorting 15 | sortedOutput.push(obj[i]); 16 | } 17 | } else { 18 | for (var prop in obj) { 19 | // push data to array for sorting 20 | sortedOutput.push([prop, obj[prop]]); 21 | } 22 | } 23 | 24 | sortedOutput.sort(function (a,b) { 25 | return b[1] - a[1]; 26 | }); 27 | 28 | return sortedOutput; 29 | 30 | }; 31 | 32 | module.exports = sortOutput; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A NodeJS console app that take an input of tags and outputs the number of instances found from scanned files. 2 | 3 | Node.js exam 4 | ==== 5 | 6 | Quick practical exam for node.js candidates. 7 | 8 | Requirements 9 | ---- 10 | 11 | - allow the user to supply a CLI argument containing a comma-separated list of tags 12 | - if no argument is given, load `tags.txt` to get an array of tags. 13 | - for each of these tags, find out how many times that tag appears within the objects in `data/*.json` (_note:_ objects can be nested). 14 | - final output should be formatted like this (sorted by most popular tag first): 15 | 16 | ``` 17 | pizza 15 18 | spoon 2 19 | umbrella 0 20 | cats 0 21 | ``` 22 | 23 | - cache the result so that subsequent runs can return the result immediately without needing to process the files. 24 | - use only core modules 25 | - use the asynchronous variants of the file IO functions (eg. use `fs.readFile` not `fs.readFileSync`). 26 | - if any of the data files contain invalid JSON, log the error with `console.error` and continue, ignoring that file. 27 | - you can use any version of node, however your solution must use plain callbacks rather than promises. 28 | -------------------------------------------------------------------------------- /modules/loadtags.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | /** 4 | * @module loadTags 5 | * @summary load tags either by detecting a comma delimited string of tags 6 | * passed in as a command line argument by the user, or by loading 7 | * them from an external '.txt' file, if no args are present 8 | */ 9 | 10 | var loadTags = function (inputtags, callback) { 11 | 12 | // check if input is a valid string 13 | if (inputtags) { 14 | // separate input tags by comma and store in an array for further processing 15 | tags = inputtags.split(","); 16 | // return an array of user provided input tags 17 | callback(null, tags); 18 | 19 | } else { 20 | // read tags.txt file and populate default tags based on it's contents 21 | fs.readFile('./tags.txt', 'utf8', function (err, data) { 22 | if (err) { 23 | // if there's an error, pass it to the callback for handling 24 | return callback(err); 25 | }else { 26 | tags = data.split('\n'); 27 | // loop through tags array and remove any empty string elements 28 | for (var i = 0, len = tags.length; i < len; i++) { 29 | if (tags[i] == "") { 30 | tags.splice(i, 1); 31 | } 32 | } 33 | // if no error, return an array of input tags to calling function 34 | callback(null, tags); 35 | } 36 | }) 37 | } 38 | 39 | }; 40 | 41 | module.exports = loadTags; 42 | -------------------------------------------------------------------------------- /modules/cachemodule.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | /** 3 | * @module Cache 4 | * @summary Simple caching module to store match results by tag 5 | * 6 | */ 7 | 8 | cachemodule = { 9 | 10 | cache: {}, 11 | 12 | setCacheItem: function (tag, matchcount, callback) { 13 | this.cache[tag] = matchcount; 14 | if (callback) { 15 | callback(this.cache[tag]); 16 | } 17 | }, 18 | 19 | getCacheItem: function (tag, callback) { 20 | if (callback) { 21 | callback(this.cache[tag], tag); 22 | } 23 | }, 24 | 25 | deleteCacheItem: function (tag) { 26 | delete this.cache[tag]; 27 | }, 28 | 29 | persistData: function (callback) { 30 | for (var prop in this.cache) { 31 | fs.appendFile("./cache/cache.txt", prop + "|" + this.cache[prop] + "\n", function(err) { 32 | if(err) { 33 | return console.log(err); 34 | } 35 | }); 36 | } 37 | if (callback) { 38 | callback(); 39 | } 40 | }, 41 | 42 | hydrateCache: function (callback) { 43 | fs.readFile("./cache/cache.txt", "utf8", function (err, data) { 44 | if (err) { 45 | //console.log("hydrate error" + err); 46 | if (callback) { 47 | callback(); 48 | } 49 | }else { 50 | if (data) { 51 | var entries = data.split('\n'); 52 | 53 | for (var i = 0, len = entries.length; i < len; i++) { 54 | // remove any empty string elements 55 | if (entries[i] == "") { 56 | entries.splice(i, 1); 57 | }else { 58 | var temparry = entries[i].split("|"); 59 | cachemodule.cache[temparry[0]] = temparry[1]; 60 | } 61 | } 62 | } 63 | if (callback) { 64 | callback(); 65 | } 66 | } 67 | }) 68 | } 69 | 70 | }; 71 | 72 | module.exports = cachemodule; 73 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var loadTags = require('./modules/loadtags'); 2 | var tagSearch = require('./modules/tagsearch'); 3 | var cachemodule = require('./modules/cachemodule'); 4 | var sortoutput = require('./modules/sortoutput'); 5 | 6 | // set placeholder array for input tags, save input to var 7 | var tags = [], 8 | cachedtags = [], 9 | noncachedtags = [], 10 | inputtags = process.argv[2], 11 | cacheCheckCt = 0; 12 | 13 | // pass user input to the loadTags module for processing, expects an array of tags to be returned 14 | loadTags(inputtags, function (err, tags) { 15 | if (err) { 16 | // display any errors in the console 17 | console.log(err); 18 | } else { 19 | // CACHING: first check to see if any of the tag search results are cached 20 | // if they are, store them in a temporary array and do not process them 21 | // through the tagsearch functionality 22 | var origTagsLen = tags.length; 23 | 24 | cachemodule.hydrateCache(function () { 25 | 26 | for (var i = 0, len = tags.length; i < len; i++) { 27 | cachemodule.getCacheItem(tags[i], function (cacheitem, thistag) { 28 | // if tag is cached 29 | if (typeof cacheitem !== 'undefined') { 30 | cachedtags.push([thistag, cacheitem]); 31 | // if tag is not cached 32 | } else { 33 | noncachedtags.push(thistag); 34 | } 35 | 36 | ++cacheCheckCt; 37 | 38 | if (cacheCheckCt == origTagsLen) { 39 | 40 | tags = noncachedtags; 41 | // check if tags array is empty (ie., all tags are already cached) 42 | if (tags.length === 0) { 43 | // write output from cached tags 44 | // sort data 45 | var sortedOutput = sortoutput(cachedtags); 46 | // print final answer to console 47 | for( var i = 0, len = sortedOutput.length; i < len; i++) { 48 | console.log(sortedOutput[i][0] + " " + sortedOutput[i][1]); 49 | } 50 | 51 | } else { 52 | // if an array of tags is returned, call the tagSearch module for further processing 53 | tagSearch(tags, function (err, data) { 54 | if (err) { 55 | // display any errors in the console 56 | console.log(err); 57 | } else { 58 | 59 | var setCacheItemCt = 0; 60 | for (var prop in data) { 61 | // cache the results for future use 62 | cachemodule.setCacheItem(prop, data[prop], function () { 63 | ++setCacheItemCt; 64 | if (setCacheItemCt == tags.length) { 65 | // persist data from cache into 'permanent' storage (txt file) 66 | cachemodule.persistData(); 67 | } 68 | }); 69 | } 70 | 71 | // merge cached tags and data if there is a mix of cached and noncached tags 72 | if (cachedtags.length > 0) { 73 | for (var i = 0, len = cachedtags.length; i < len; i++) { 74 | data[cachedtags[i][0]] = cachedtags[i][1]; 75 | } 76 | } 77 | 78 | // sort data 79 | var sortedOutput = sortoutput(data); 80 | // print final answer to console 81 | for( var i = 0, len = sortedOutput.length; i < len; i++) { 82 | console.log(sortedOutput[i][0] + " " + sortedOutput[i][1]) 83 | } 84 | } 85 | }); 86 | } 87 | } 88 | }); 89 | } 90 | }); 91 | } 92 | }) 93 | -------------------------------------------------------------------------------- /modules/tagsearch.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | /** 4 | * @module searchTags 5 | * @summary Searches json files in the data folder and determines how many matches 6 | * with the input tags are present 7 | */ 8 | 9 | // returns an object containing tags (key) and their match count (value) 10 | var tagSearch = function (tags, callback) { 11 | // results object to be returned to callee 12 | // key is tag name, value is number of matches 13 | var results = {}, 14 | searchFilesCt = 0; 15 | 16 | // get list of files from data directory 17 | fs.readdir('./data', function (err, files) { 18 | if (err) { 19 | // if there's an error, pass it to the callback for handling 20 | return callback(err); 21 | } else { 22 | // for each tag, run a file search 23 | for (var j = 0, len = tags.length; j < len; j++) { 24 | 25 | // call the main search function, passing in the current tag and list of files, 26 | // the callback sets the match counts and tags into the results object 27 | searchFiles(files, tags[j], function(err, pair) { 28 | if (err) { 29 | console.log(err); 30 | } else { 31 | // add match count for current tag to results object 32 | results[pair[0]] = pair[1]; 33 | ++searchFilesCt; 34 | if (searchFilesCt === tags.length) { 35 | // return results object back to app.js 36 | callback(null, results); 37 | } 38 | } 39 | }); 40 | } 41 | } 42 | }); 43 | 44 | // search the contets of each file for tag matches 45 | // key = prop, val = obj[prop] 46 | var parseFile = function (obj, tag, count) { 47 | 48 | // key value processing helper function 49 | var keyvalprocess = function (key, value) { 50 | if (key == tag) { 51 | count++; 52 | } 53 | if (value == tag) { 54 | count++; 55 | } 56 | } 57 | 58 | // recursive function to loop through object properties 59 | var parsejson = function (obj, func) { 60 | for (var prop in obj) { 61 | // if value is an object, don't evaluate it before looping iterating over it's key / vals 62 | if (!(typeof(obj[prop])=="object")) { 63 | func.apply(this, [prop, obj[prop]]); 64 | } 65 | if (obj[prop] !== null && typeof(obj[prop])=="object") { 66 | // one level deeper 67 | parsejson(obj[prop],func); 68 | } 69 | } 70 | } 71 | parsejson(obj, keyvalprocess); 72 | return count; 73 | }; 74 | 75 | // identify list of files to scan and iterate through them, 76 | // verifying that their contents is a valid json object 77 | var searchFiles = function (files, tag, callback) { 78 | 79 | // keep track of match counts per tag 80 | var count = 0, 81 | counter = 0; 82 | // iterate through the list of files 83 | for (var i = 0, len = files.length; i < len; i++) { 84 | 85 | // read each file in the 'data' folder 86 | // increment per-tag match count for each file scanned 87 | var incrementMatchCount = function (tagMatchesFromFile) { 88 | count += tagMatchesFromFile; 89 | ++counter; 90 | if (counter === files.length) { 91 | // send back match count for the current tag 92 | var thisPair = [tag, count]; 93 | callback(null, thisPair); 94 | } 95 | }; 96 | 97 | var readCurrentFile = function (callback) { 98 | 99 | fs.readFile('./data/' + files[i], 'utf8', function (err, data) { 100 | if (err) { 101 | // if there's an error console log the error 102 | return callback(err); 103 | } else { 104 | // convert buffer string into json object 105 | try { 106 | data = JSON.parse(data); 107 | } catch (e) { 108 | // not valid json - ignore file and log error 109 | console.error("Invalid JSON: " + e) 110 | } 111 | // verify data is an object 112 | if (typeof data == "object") { 113 | // call 'parseFile' function to parse json in each file, looking for tag matches 114 | var tagMatchesFromFile = parseFile(data, tag, 0); 115 | callback(tagMatchesFromFile); 116 | } else { 117 | var tagMatchesFromFile = 0; 118 | callback(tagMatchesFromFile); 119 | } 120 | } 121 | }); 122 | }; 123 | 124 | readCurrentFile(incrementMatchCount); 125 | } 126 | }; 127 | }; 128 | 129 | module.exports = tagSearch; 130 | --------------------------------------------------------------------------------