├── .travis.yml ├── .gitignore ├── test ├── unit │ ├── sync_fuzz.js │ ├── interface.js │ ├── gh_13.js │ ├── gh_12.js │ ├── sync_corpus.js │ ├── sync_positive.js │ ├── sync_inject.js │ ├── sync_negative.js │ ├── async_positive.js │ └── async_negative.js ├── benchmark │ └── index.js └── fixtures │ ├── fuzz.js │ └── corpus.js ├── .jshintrc ├── makefile ├── package.json ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md └── lib └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /* MacOS */ 2 | .DS_Store 3 | 4 | /* NPM */ 5 | /node_modules 6 | npm* 7 | 8 | /* Other */ 9 | .notes.txt -------------------------------------------------------------------------------- /test/unit/sync_fuzz.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var fuzz = require('../fixtures/fuzz'); 3 | var sentiment = require('../../lib/index'); 4 | 5 | var dataset = fuzz(1000); 6 | 7 | test('synchronous fuzz', function (t) { 8 | t.type(sentiment(dataset), 'object'); 9 | t.end(); 10 | }); -------------------------------------------------------------------------------- /test/unit/interface.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var sentiment = require('../../lib/index'); 3 | 4 | test('interface', function (t) { 5 | t.type(sentiment, 'function', 'module is a function'); 6 | t.type(sentiment('test'), 'object'); 7 | t.type(sentiment('test', {test: 10}), 'object'); 8 | t.end(); 9 | }); -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "devel": true, 4 | "eqeqeq": true, 5 | "immed": true, 6 | "maxdepth": 2, 7 | "maxparams": 3, 8 | "newcap": true, 9 | "noarg": true, 10 | "node": true, 11 | "proto": true, 12 | "quotmark": "single", 13 | "undef": true, 14 | "unused": true, 15 | "maxlen": 80 16 | } -------------------------------------------------------------------------------- /test/unit/gh_13.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var sentiment = require('../../lib/index'); 3 | 4 | var dataset = 'constructor'; 5 | var result = sentiment(dataset); 6 | console.dir(result); 7 | 8 | test('synchronous positive', function (t) { 9 | t.type(result, 'object'); 10 | t.equal(result.score, 0); 11 | t.equal(result.comparative, 0); 12 | t.equal(result.tokens.length, 1); 13 | t.equal(result.words.length, 0); 14 | t.end(); 15 | }); 16 | -------------------------------------------------------------------------------- /test/unit/gh_12.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var sentiment = require('../../lib/index'); 3 | 4 | var dataset = 'self-deluded'; 5 | var result = sentiment(dataset); 6 | console.dir(result); 7 | 8 | test('synchronous positive', function (t) { 9 | t.type(result, 'object'); 10 | t.equal(result.score, -2); 11 | t.equal(result.comparative, -2); 12 | t.equal(result.tokens.length, 1); 13 | t.equal(result.words.length, 1); 14 | t.end(); 15 | }); 16 | -------------------------------------------------------------------------------- /test/unit/sync_corpus.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var corpus = require('../fixtures/corpus'); 3 | var sentiment = require('../../lib/index'); 4 | 5 | var dataset = corpus; 6 | var result = sentiment(dataset); 7 | console.dir(result); 8 | 9 | test('synchronous corpus', function (t) { 10 | t.type(result, 'object'); 11 | t.equal(result.score, -7); 12 | t.equal(result.tokens.length, 1416); 13 | t.equal(result.words.length, 72); 14 | t.end(); 15 | }); -------------------------------------------------------------------------------- /test/unit/sync_positive.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var sentiment = require('../../lib/index'); 3 | 4 | var dataset = 'This is so cool'; 5 | var result = sentiment(dataset); 6 | console.dir(result); 7 | 8 | test('synchronous positive', function (t) { 9 | t.type(result, 'object'); 10 | t.equal(result.score, 1); 11 | t.equal(result.comparative, 0.25); 12 | t.equal(result.tokens.length, 4); 13 | t.equal(result.words.length, 1); 14 | t.end(); 15 | }); -------------------------------------------------------------------------------- /test/unit/sync_inject.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var sentiment = require('../../lib/index'); 3 | 4 | var dataset = 'This is so cool'; 5 | var result = sentiment(dataset, {'cool': 100}); 6 | console.dir(result); 7 | 8 | test('synchronous inject', function (t) { 9 | t.type(result, 'object'); 10 | t.equal(result.score, 100); 11 | t.equal(result.comparative, 25); 12 | t.equal(result.tokens.length, 4); 13 | t.equal(result.words.length, 1); 14 | t.end(); 15 | }); -------------------------------------------------------------------------------- /test/unit/sync_negative.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var sentiment = require('../../lib/index'); 3 | 4 | var dataset = 'Hey you worthless scumbag'; 5 | var result = sentiment(dataset); 6 | console.dir(result); 7 | 8 | test('synchronous negative', function (t) { 9 | t.type(result, 'object'); 10 | t.equal(result.score, -6); 11 | t.equal(result.comparative, -1.5); 12 | t.equal(result.tokens.length, 4); 13 | t.equal(result.words.length, 2); 14 | t.end(); 15 | }); -------------------------------------------------------------------------------- /test/unit/async_positive.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var sentiment = require('../../lib/index'); 3 | 4 | var dataset = 'This is so cool'; 5 | sentiment(dataset, function (err, result) { 6 | console.dir(err); 7 | console.dir(result); 8 | 9 | test('asynchronous positive', function (t) { 10 | t.type(result, 'object'); 11 | t.equal(result.score, 1); 12 | t.equal(result.comparative, 0.25); 13 | t.equal(result.tokens.length, 4); 14 | t.equal(result.words.length, 1); 15 | t.end(); 16 | }); 17 | }); -------------------------------------------------------------------------------- /test/unit/async_negative.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var sentiment = require('../../lib/index'); 3 | 4 | var dataset = 'Hey you worthless scumbag'; 5 | sentiment(dataset, function (err, result) { 6 | console.dir(err); 7 | console.dir(result); 8 | 9 | test('asynchronous positive', function (t) { 10 | t.type(result, 'object'); 11 | t.equal(result.score, -6); 12 | t.equal(result.comparative, -1.5); 13 | t.equal(result.tokens.length, 4); 14 | t.equal(result.words.length, 2); 15 | t.end(); 16 | }); 17 | }); -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Paths 2 | TAP = ./node_modules/.bin/tap 3 | JSHINT = ./node_modules/.bin/jshint 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | # Builds a JSON representation of the raw AFINN word list 8 | build: 9 | node ./build/index.js 10 | 11 | # Governance tests 12 | lint: 13 | $(JSHINT) ./lib/*.js 14 | $(JSHINT) ./build/*.js 15 | $(JSHINT) ./test/**/*.js 16 | 17 | # Unit tests 18 | unit: 19 | $(TAP) ./test/unit/*.js 20 | 21 | # Benchmarks 22 | benchmark: 23 | node test/benchmark/index.js 24 | 25 | # Run entire test suite 26 | test: 27 | @make lint 28 | @make unit 29 | @make benchmark 30 | 31 | # ------------------------------------------------------------------------------ 32 | 33 | .PHONY: build lint unit benchmark test -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "John Thillaye", 3 | "name": "sentiment-french", 4 | "description": "AFINN-based sentiment analysis for Node.js, in French.", 5 | "version": "1.0.2", 6 | "homepage": "https://github.com/johnthillaye/sentiment", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/johnthillaye/sentiment.git" 10 | }, 11 | "keywords": [ 12 | "sentiment", 13 | "analysis", 14 | "nlp", 15 | "sentiment analysis" 16 | ], 17 | "main": "./lib/index.js", 18 | "scripts": { 19 | "test": "make test" 20 | }, 21 | "dependencies": { 22 | "extend-object": "~1.0.0" 23 | }, 24 | "devDependencies": { 25 | "benchmark": "~1.0.0", 26 | "jshint": "~2.5.0", 27 | "tap": "~0.4.8", 28 | "Sentimental": "1.0.1" 29 | }, 30 | "engines": { 31 | "node": ">=0.10" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/benchmark/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs benchmarks against sentiment and Sentimental. 3 | * 4 | * @package sentiment 5 | * @author Andrew Sliwinski 6 | */ 7 | 8 | /** 9 | * Dependencies 10 | */ 11 | var Benchmark = require('benchmark'); 12 | var suite = new Benchmark.Suite(); 13 | 14 | var sentiment = require('../../lib/index'); 15 | var sentimental = require('Sentimental'); 16 | 17 | /** 18 | * Test data 19 | */ 20 | var stringShort = 'This cat is totally awesome'; 21 | 22 | /** 23 | * Setup 24 | */ 25 | suite 26 | .add('sentiment (Latest)', function () { 27 | sentiment(stringShort); 28 | }) 29 | .add('Sentimental (1.0.1)', function () { 30 | sentimental.analyze(stringShort); 31 | }) 32 | .on('cycle', function (event) { 33 | console.log(String(event.target)); 34 | }) 35 | .run({ 36 | minSamples: 100, 37 | delay: 2 38 | }); -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. In that way, this project is more like a wiki than a standard guarded open source project. 4 | 5 | ## Rules 6 | 7 | There are a few basic ground-rules for contributors: 8 | 9 | 1. **No `--force` pushes** or modifying the Git history in any way. 10 | 1. **Non-master branches** ought to be used for ongoing work. 11 | 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 12 | 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 13 | 1. Contributors should attempt to adhere to the prevailing code-style. 14 | 15 | ## Releases 16 | 17 | Declaring formal releases remains the prerogative of the project maintainer. 18 | -------------------------------------------------------------------------------- /test/fixtures/fuzz.js: -------------------------------------------------------------------------------- 1 | function rand (limit) { 2 | return Math.floor(Math.random() * limit); 3 | } 4 | 5 | function createRandomWord (length) { 6 | var consonants = 'bcdfghjklmnpqrstvwxyz!@#$%^&*()_+":;\'?><~`'; 7 | var vowels = 'aeiou'; 8 | var word = ''; 9 | 10 | // Split 11 | consonants = consonants.split(''); 12 | vowels = vowels.split(''); 13 | 14 | // Create word 15 | for (var i = 0; i < length / 2; i++) { 16 | var randConsonant = consonants[rand(consonants.length)]; 17 | var randVowel = vowels[rand(vowels.length)]; 18 | 19 | word += (i===0) ? randConsonant.toUpperCase() : randConsonant; 20 | word += i*2 *Translated in French* 5 | 6 | [![Build Status](https://secure.travis-ci.org/thisandagain/sentiment.png)](http://travis-ci.org/thisandagain/sentiment) 7 | 8 | Sentiment is a Node.js module that uses the [AFINN-111](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6010) wordlist to perform [sentiment analysis](http://en.wikipedia.org/wiki/Sentiment_analysis) on arbitrary blocks of input text. Sentiment provides serveral things: 9 | 10 | - Performance (see benchmarks below) 11 | - The ability to append and overwrite word / value pairs from the AFINN wordlist 12 | - A build process that makes updating sentiment to future versions of the AFINN word list trivial 13 | 14 | ### Installation 15 | ```bash 16 | npm install sentiment-french 17 | ``` 18 | 19 | ### Usage 20 | ```javascript 21 | var sentiment = require('sentiment'); 22 | 23 | var r1 = sentiment('Cats are stupid.'); 24 | console.dir(r1); // Score: -2, Comparative: -0.666 25 | 26 | var r2 = sentiment('Cats are totally amazing!'); 27 | console.dir(r2); // Score: 4, Comparative: 1 28 | ``` 29 | 30 | ### Adding / overwriting words 31 | You can append and/or overwrite values from AFINN by simply injecting key/value pairs into a sentiment method call: 32 | ```javascript 33 | var sentiment = require('sentiment'); 34 | 35 | var result = sentiment('Cats are totally amazing!', { 36 | 'cats': 5, 37 | 'amazing': 2 38 | }); 39 | console.dir(result); // Score: 7, Comparative: 1.75 40 | ``` 41 | 42 | --- 43 | 44 | ### Benchmarks 45 | The primary motivation for designing `sentiment` was performance. As such, it includes a benchmark script within the test directory that compares it against the [Sentimental](https://github.com/thinkroth/Sentimental) module which provides a nearly equivalent interface and approach. Based on these benchmarks, running on an older MacBook Air with Node 0.10.26, `sentiment` is **more than twice as fast** as alternative implementations: 46 | 47 | ```bash 48 | sentiment (Latest) x 244,901 ops/sec ±0.49% (100 runs sampled) 49 | Sentimental (1.0.1) x 94,135 ops/sec ±0.50% (100 runs sampled) 50 | ``` 51 | 52 | To run the benchmarks yourself, simply: 53 | ```bash 54 | make benchmark 55 | ``` 56 | 57 | --- 58 | 59 | ### Testing 60 | ```bash 61 | npm test 62 | ``` 63 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AFINN-based sentiment analysis for Node.js 3 | * 4 | * @package sentiment 5 | * @author Andrew Sliwinski 6 | */ 7 | 8 | /** 9 | * Dependencies 10 | */ 11 | var extend = require('extend-object'); 12 | var afinn = require('../build/AFINN.json'); 13 | 14 | /** 15 | * Tokenizes an input string. 16 | * 17 | * @param {String} Input 18 | * 19 | * @return {Array} 20 | */ 21 | function tokenize (input) { 22 | return input 23 | .replace(/[^a-zA-Z- ]+/g, '') 24 | .replace('/ {2,}/',' ') 25 | .toLowerCase() 26 | .split(' '); 27 | } 28 | 29 | /** 30 | * Performs sentiment analysis on the provided input "phrase". 31 | * 32 | * @param {String} Input phrase 33 | * @param {Object} Optional sentiment additions to AFINN (hash k/v pairs) 34 | * 35 | * @return {Object} 36 | */ 37 | module.exports = function (phrase, inject, callback) { 38 | // Parse arguments 39 | if (typeof phrase === 'undefined') phrase = ''; 40 | if (typeof inject === 'undefined') inject = null; 41 | if (typeof inject === 'function') callback = inject; 42 | if (typeof callback === 'undefined') callback = null; 43 | 44 | // Merge 45 | if (inject !== null) { 46 | afinn = extend(afinn, inject); 47 | } 48 | 49 | // Storage objects 50 | var tokens = tokenize(phrase), 51 | score = 0, 52 | words = [], 53 | positive = [], 54 | negative = []; 55 | 56 | // Iterate over tokens 57 | var len = tokens.length; 58 | while (len--) { 59 | var obj = tokens[len]; 60 | var item = afinn[obj]; 61 | if (!afinn.hasOwnProperty(obj)) continue; 62 | 63 | words.push(obj); 64 | if (item > 0) positive.push(obj); 65 | if (item < 0) negative.push(obj); 66 | 67 | score += item; 68 | } 69 | 70 | // Handle optional async interface 71 | var result = { 72 | score: score, 73 | comparative: score / tokens.length, 74 | tokens: tokens, 75 | words: words, 76 | positive: positive, 77 | negative: negative 78 | }; 79 | 80 | if (callback === null) return result; 81 | process.nextTick(function () { 82 | callback(null, result); 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /test/fixtures/corpus.js: -------------------------------------------------------------------------------- 1 | /* jshint maxlen: false, quotmark: false */ 2 | 3 | // http://www.gutenberg.org/files/2701/2701-h/2701-h.htm 4 | module.exports = "I stuffed a shirt or two into my old carpet-bag, tucked it under my arm, and started for Cape Horn and the Pacific. Quitting the good city of old Manhatto, I duly arrived in New Bedford. It was a Saturday night in December. Much was I disappointed upon learning that the little packet for Nantucket had already sailed, and that no way of reaching that place would offer, till the following Monday. As most young candidates for the pains and penalties of whaling stop at this same New Bedford, thence to embark on their voyage, it may as well be related that I, for one, had no idea of so doing. For my mind was made up to sail in no other than a Nantucket craft, because there was a fine, boisterous something about everything connected with that famous old island, which amazingly pleased me. Besides though New Bedford has of late been gradually monopolising the business of whaling, and though in this matter poor old Nantucket is now much behind her, yet Nantucket was her great original—the Tyre of this Carthage;—the place where the first dead American whale was stranded. Where else but from Nantucket did those aboriginal whalemen, the Red-Men, first sally out in canoes to give chase to the Leviathan? And where but from Nantucket, too, did that first adventurous little sloop put forth, partly laden with imported cobblestones—so goes the story—to throw at the whales, in order to discover when they were nigh enough to risk a harpoon from the bowsprit? Now having a night, a day, and still another night following before me in New Bedford, ere I could embark for my destined port, it became a matter of concernment where I was to eat and sleep meanwhile. It was a very dubious-looking, nay, a very dark and dismal night, bitingly cold and cheerless. I knew no one in the place. With anxious grapnels I had sounded my pocket, and only brought up a few pieces of silver,—So, wherever you go, Ishmael, said I to myself, as I stood in the middle of a dreary street shouldering my bag, and comparing the gloom towards the north with the darkness towards the south—wherever in your wisdom you may conclude to lodge for the night, my dear Ishmael, be sure to inquire the price, and don't be too particular. With halting steps I paced the streets, and passed the sign of \"The Crossed Harpoons\"—but it looked too expensive and jolly there. Further on, from the bright red windows of the \"Sword-Fish Inn,\" there came such fervent rays, that it seemed to have melted the packed snow and ice from before the house, for everywhere else the congealed frost lay ten inches thick in a hard, asphaltic pavement,—rather weary for me, when I struck my foot against the flinty projections, because from hard, remorseless service the soles of my boots were in a most miserable plight. Too expensive and jolly, again thought I, pausing one moment to watch the broad glare in the street, and hear the sounds of the tinkling glasses within. But go on, Ishmael, said I at last; don't you hear? get away from before the door; your patched boots are stopping the way. So on I went. I now by instinct followed the streets that took me waterward, for there, doubtless, were the cheapest, if not the cheeriest inns. Such dreary streets! blocks of blackness, not houses, on either hand, and here and there a candle, like a candle moving about in a tomb. At this hour of the night, of the last day of the week, that quarter of the town proved all but deserted. But presently I came to a smoky light proceeding from a low, wide building, the door of which stood invitingly open. It had a careless look, as if it were meant for the uses of the public; so, entering, the first thing I did was to stumble over an ash-box in the porch. Ha! thought I, ha, as the flying particles almost choked me, are these ashes from that destroyed city, Gomorrah? But \"The Crossed Harpoons,\" and \"The Sword-Fish?\"—this, then must needs be the sign of \"The Trap.\" However, I picked myself up and hearing a loud voice within, pushed on and opened a second, interior door. It seemed the great Black Parliament sitting in Tophet. A hundred black faces turned round in their rows to peer; and beyond, a black Angel of Doom was beating a book in a pulpit. It was a negro church; and the preacher's text was about the blackness of darkness, and the weeping and wailing and teeth-gnashing there. Ha, Ishmael, muttered I, backing out, Wretched entertainment at the sign of 'The Trap!' Moving on, I at last came to a dim sort of light not far from the docks, and heard a forlorn creaking in the air; and looking up, saw a swinging sign over the door with a white painting upon it, faintly representing a tall straight jet of misty spray, and these words underneath—\"The Spouter Inn:—Peter Coffin.\" Coffin?—Spouter?—Rather ominous in that particular connexion, thought I. But it is a common name in Nantucket, they say, and I suppose this Peter here is an emigrant from there. As the light looked so dim, and the place, for the time, looked quiet enough, and the dilapidated little wooden house itself looked as if it might have been carted here from the ruins of some burnt district, and as the swinging sign had a poverty-stricken sort of creak to it, I thought that here was the very spot for cheap lodgings, and the best of pea coffee. It was a queer sort of place—a gable-ended old house, one side palsied as it were, and leaning over sadly. It stood on a sharp bleak corner, where that tempestuous wind Euroclydon kept up a worse howling than ever it did about poor Paul's tossed craft. Euroclydon, nevertheless, is a mighty pleasant zephyr to any one in-doors, with his feet on the hob quietly toasting for bed. \"In judging of that tempestuous wind called Euroclydon,\" says an old writer—of whose works I possess the only copy extant—\"it maketh a marvellous difference, whether thou lookest out at it from a glass window where the frost is all on the outside, or whether thou observest it from that sashless window, where the frost is on both sides, and of which the wight Death is the only glazier.\" True enough, thought I, as this passage occurred to my mind—old black-letter, thou reasonest well. Yes, these eyes are windows, and this body of mine is the house. What a pity they didn't stop up the chinks and the crannies though, and thrust in a little lint here and there. But it's too late to make any improvements now. The universe is finished; the copestone is on, and the chips were carted off a million years ago. Poor Lazarus there, chattering his teeth against the curbstone for his pillow, and shaking off his tatters with his shiverings, he might plug up both ears with rags, and put a corn-cob into his mouth, and yet that would not keep out the tempestuous Euroclydon. Euroclydon! says old Dives, in his red silken wrapper—(he had a redder one afterwards) pooh, pooh! What a fine frosty night; how Orion glitters; what northern lights! Let them talk of their oriental summer climes of everlasting conservatories; give me the privilege of making my own summer with my own coals. But what thinks Lazarus? Can he warm his blue hands by holding them up to the grand northern lights? Would not Lazarus rather be in Sumatra than here? Would he not far rather lay him down lengthwise along the line of the equator; yea, ye gods! go down to the fiery pit itself, in order to keep out this frost? Now, that Lazarus should lie stranded there on the curbstone before the door of Dives, this is more wonderful than that an iceberg should be moored to one of the Moluccas. Yet Dives himself, he too lives like a Czar in an ice palace made of frozen sighs, and being a president of a temperance society, he only drinks the tepid tears of orphans. But no more of this blubbering now, we are going a-whaling, and there is plenty of that yet to come. Let us scrape the ice from our frosted feet, and see what sort of a place this \"Spouter\" may be."; --------------------------------------------------------------------------------