natural 0.5.4 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.dockerignore +2 -0
  2. package/.npmignore +1 -0
  3. package/.travis.yml +1 -1
  4. package/README.md +412 -89
  5. package/bayes_classifier.json +1 -1
  6. package/classifier.json +418 -0
  7. package/data/kata-dasar.txt +29933 -0
  8. package/lib/natural/brill_pos_tagger/lib/Brill_POS_Tagger.js +8 -9
  9. package/lib/natural/brill_pos_tagger/lib/Brill_POS_Tester.js +5 -5
  10. package/lib/natural/brill_pos_tagger/lib/Brill_POS_Trainer.js +10 -6
  11. package/lib/natural/brill_pos_tagger/lib/Corpus.js +21 -16
  12. package/lib/natural/brill_pos_tagger/lib/Lexicon.js +22 -13
  13. package/lib/natural/brill_pos_tagger/lib/Predicate.js +2 -2
  14. package/lib/natural/brill_pos_tagger/lib/RuleTemplate.js +2 -2
  15. package/lib/natural/brill_pos_tagger/lib/RuleTemplates.js +132 -146
  16. package/lib/natural/brill_pos_tagger/lib/Sentence.js +41 -0
  17. package/lib/natural/brill_pos_tagger/lib/TF_Parser.js +83 -76
  18. package/lib/natural/brill_pos_tagger/lib/TF_Parser.pegjs +19 -4
  19. package/lib/natural/brill_pos_tagger/lib/TransformationRule.js +10 -11
  20. package/lib/natural/classifiers/classifier.js +15 -8
  21. package/lib/natural/classifiers/maxent/Classifier.js +125 -0
  22. package/lib/natural/classifiers/maxent/Context.js +33 -0
  23. package/lib/natural/classifiers/maxent/Distribution.js +145 -0
  24. package/lib/natural/classifiers/maxent/Element.js +34 -0
  25. package/lib/natural/classifiers/maxent/Feature.js +88 -0
  26. package/lib/natural/classifiers/maxent/FeatureSet.js +67 -0
  27. package/lib/natural/classifiers/maxent/GISScaler.js +148 -0
  28. package/lib/natural/classifiers/maxent/POS/ME_Corpus.js +56 -0
  29. package/lib/natural/classifiers/maxent/POS/ME_Sentence.js +73 -0
  30. package/lib/natural/classifiers/maxent/POS/POS_Element.js +172 -0
  31. package/lib/natural/classifiers/maxent/Sample.js +141 -0
  32. package/lib/natural/classifiers/maxent/SimpleExample/SE_Element.js +49 -0
  33. package/lib/natural/distance/hamming_distance.js +52 -0
  34. package/lib/natural/distance/jaro-winkler_distance.js +16 -6
  35. package/lib/natural/distance/levenshtein_distance.js +164 -11
  36. package/lib/natural/index.js +26 -5
  37. package/lib/natural/inflectors/noun_inflector.js +15 -13
  38. package/lib/natural/ngrams/ngrams.js +83 -25
  39. package/lib/natural/normalizers/normalizer_sv.js +41 -0
  40. package/lib/natural/sentiment/Basque/senticon_eu.json +21557 -0
  41. package/lib/natural/sentiment/Catalan/senticon_ca.json +36352 -0
  42. package/lib/natural/sentiment/Dutch/negations_du.json +3 -0
  43. package/lib/natural/sentiment/Dutch/pattern-sentiment-nl.json +39650 -0
  44. package/lib/natural/sentiment/English/negations_en.json +3 -0
  45. package/lib/natural/sentiment/English/pattern-sentiment-en.json +16024 -0
  46. package/lib/natural/sentiment/English/senticon_en.json +124197 -0
  47. package/lib/natural/sentiment/French/pattern-sentiment-fr.json +40906 -0
  48. package/lib/natural/sentiment/Galician/senticon_gl.json +24427 -0
  49. package/lib/natural/sentiment/Italian/pattern-sentiment-it.json +24522 -0
  50. package/lib/natural/sentiment/SentimentAnalyzer.js +116 -0
  51. package/lib/natural/sentiment/Spanish/afinnShortSortedSpanish.json +1655 -0
  52. package/lib/natural/sentiment/Spanish/negations_es.json +3 -0
  53. package/lib/natural/sentiment/Spanish/senticon_es.json +56722 -0
  54. package/lib/natural/sentiment/tools/README.md +31 -0
  55. package/lib/natural/sentiment/tools/XmlParser4PatternData.js +31 -0
  56. package/lib/natural/sentiment/tools/sentimentXmlParser.js +30 -0
  57. package/lib/natural/stemmers/indonesian/base_stemmer_id.js +86 -0
  58. package/lib/natural/stemmers/indonesian/prefix_rules.js +1027 -0
  59. package/lib/natural/stemmers/indonesian/removal.js +49 -0
  60. package/lib/natural/stemmers/indonesian/stemmer_id.js +307 -0
  61. package/lib/natural/stemmers/indonesian/suffix_rules.js +75 -0
  62. package/lib/natural/stemmers/porter_stemmer_es.js +265 -199
  63. package/lib/natural/stemmers/porter_stemmer_fr.js +1 -1
  64. package/lib/natural/stemmers/porter_stemmer_nl.js +362 -0
  65. package/lib/natural/stemmers/porter_stemmer_sv.js +99 -0
  66. package/lib/natural/stemmers/stemmer_es.js +18 -12
  67. package/lib/natural/stemmers/stemmer_nl.js +64 -0
  68. package/lib/natural/stemmers/stemmer_sv.js +61 -0
  69. package/lib/natural/tfidf/tfidf.js +27 -4
  70. package/lib/natural/tokenizers/aggressive_tokenizer_id.js +44 -0
  71. package/lib/natural/tokenizers/aggressive_tokenizer_sv.js +40 -0
  72. package/lib/natural/tokenizers/aggressive_tokenizer_vi.js +37 -0
  73. package/lib/natural/tokenizers/orthography_matchers.js +33 -0
  74. package/lib/natural/tokenizers/regexp_tokenizer.js +23 -3
  75. package/lib/natural/tokenizers/sentence_tokenizer.js +1 -1
  76. package/lib/natural/util/stopwords_id.js +119 -0
  77. package/lib/natural/util/stopwords_no.js +1 -1
  78. package/lib/natural/util/stopwords_sv.js +70 -0
  79. package/lib/natural/wordnet/wordnet_file.js +7 -1
  80. package/lr_classifier.json +1 -1
  81. package/package-lock.json +378 -0
  82. package/package.json +29 -21
@@ -0,0 +1,172 @@
1
+ /*
2
+ Element class for POS tagging
3
+ Copyright (C) 2018 Hugo W.L. ter Doest
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+
19
+
20
+ var util = require('util');
21
+ var Element = require('../Element');
22
+ var Feature = require('../Feature');
23
+
24
+ function POS_Element(a, b) {
25
+ POS_Element.super_.call(this, a, b);
26
+ }
27
+
28
+ util.inherits(POS_Element, Element);
29
+
30
+ POS_Element.prototype.generateFeatures = function(featureSet) {
31
+ var context = this.b.data;
32
+ var tag = this.a;
33
+ var token = context.wordWindow["0"];
34
+
35
+
36
+ // Feature for the current word
37
+ function currentWord(x) {
38
+ if ((x.b.data.wordWindow["0"] === token) &&
39
+ (x.a === tag)) {
40
+ return 1;
41
+ }
42
+ return 0;
43
+ }
44
+ featureSet.addFeature(new Feature(currentWord, "wordFeature", ["0", token, "0", tag]));
45
+
46
+
47
+ // Feature for previous bigram (previous two tags), positions -2, -1
48
+ if (context.tagWindow["-2"]) {
49
+ var prevPrevTag = context.tagWindow["-2"];
50
+ var prevTag = context.tagWindow["-1"];
51
+ function prevBigram(x) {
52
+ if ((x.a === tag) &&
53
+ (x.b.data.tagWindow["-2"] === prevPrevTag) &&
54
+ (x.b.data.tagWindow["-1"] === prevTag)) {
55
+ return 1;
56
+ }
57
+ return 0;
58
+ }
59
+ featureSet.addFeature(new Feature(prevBigram, "prevBigram", ["0", tag, "-2", prevPrevTag, "-1", prevTag]));
60
+ }
61
+
62
+
63
+ /*
64
+ // Feature for left bigram, positions -1, 0
65
+ if (context.tagWindow["-1"]) {
66
+ var prevTag = context.tagWindow["-1"];
67
+ function leftBigram(x) {
68
+ if ((x.b.data.tagWindow["-1"] === prevTag) &&
69
+ (x.a === tag)) {
70
+ return 1;
71
+ }
72
+ return 0;
73
+ }
74
+ featureSet.addFeature(new Feature(leftBigram, "leftBigram", ["0", tag, "-1", prevTag]));
75
+ }
76
+ */
77
+
78
+ /*
79
+
80
+ // Feature for right bigram, positions 0, 1
81
+ if (context.tagWindow["1"]) {
82
+ var nextTag = context.tagWindow["1"];
83
+ function rightBigram(x) {
84
+ if ((x.a === tag) &&
85
+ (x.b.data.tagWindow["1"] === nextTag)) {
86
+ return 1;
87
+ }
88
+ return 0;
89
+ }
90
+ featureSet.addFeature(new Feature(rightBigram, "rightBigram", ["0", tag, "1", nextTag]));
91
+ }
92
+ */
93
+ /*
94
+ // Feature for next bigram (next two tags), positions 1 and 2
95
+ if (context.tagWindow["2"]) {
96
+ var nextTag = context.tagWindow["1"];
97
+ var nextNextTag = context.tagWindow["2"];
98
+ function nextBigram(x) {
99
+ if ((x.a === tag) &&
100
+ (x.b.data.tagWindow["1"] === nextTag) &&
101
+ (x.b.data.tagWindow["2"] === nextNextTag)) {
102
+ return 1;
103
+ }
104
+ return 0;
105
+ }
106
+ featureSet.addFeature(new Feature(nextBigram, "nextBigram", ["0", tag, "1", nextTag, "2", nextNextTag]));
107
+ }
108
+
109
+ // Feature that looks at the left bigram words
110
+ if (context.wordWindow["-1"]) {
111
+ var prevWord = context.wordWindow["-1"];
112
+ function leftBigramWords(x) {
113
+ if ((x.a === tag) &&
114
+ (x.b.data.wordWindow["0"] === token) &&
115
+ (x.b.data.wordWindow["-1"] === prevWord)) {
116
+ return 1;
117
+ }
118
+ return 0;
119
+ }
120
+ featureSet.addFeature(new Feature(leftBigramWords, "leftBigramWords", ["0", tag, "0", token, "-1", prevWord]));
121
+ }
122
+
123
+ // Feature that looks at the right bigram words
124
+ if (context.wordWindow["1"]) {
125
+ var nextWord = context.wordWindow["1"];
126
+ function rightBigramWords(x) {
127
+ if ((x.a === tag) &&
128
+ (x.b.data.wordWindow["0"] === token) &&
129
+ (x.b.data.wordWindow["1"] === nextWord)) {
130
+ return 1;
131
+ }
132
+ return 0;
133
+ }
134
+ featureSet.addFeature(new Feature(rightBigramWords, "rightBigramWords", ["0", tag, "0", token, "1", nextWord]));
135
+ }
136
+ */
137
+
138
+ // Feature that looks at the previous word and its category
139
+ if (context.wordWindow["-1"]) {
140
+ var prevWord = context.wordWindow["-1"];
141
+ var prevTag = context.tagWindow["-1"];
142
+ function prevWordAndCat(x) {
143
+ if ((x.a === tag) &&
144
+ (x.b.data.wordWindow["-1"] === prevWord) &&
145
+ (x.b.data.tagWindow["-1"] === prevTag)) {
146
+ return 1;
147
+ }
148
+ return 0;
149
+ }
150
+ featureSet.addFeature(new Feature(prevWordAndCat, "prevWordAndCat", ["0", tag, "-1", prevWord, "-1", prevTag]));
151
+ }
152
+
153
+
154
+ /*
155
+ // Feature that looks at the next word and its category
156
+ if (context.wordWindow["1"]) {
157
+ var nextWord = context.wordWindow["1"];
158
+ var nextTag = context.tagWindow["1"];
159
+ function nextWordAndCat(x) {
160
+ if ((x.a === tag) &&
161
+ (x.b.data.wordWindow["1"] === nextWord) &&
162
+ (x.b.data.tagWindow["1"] === nextTag)) {
163
+ return 1;
164
+ }
165
+ return 0;
166
+ }
167
+ featureSet.addFeature(new Feature(nextWordAndCat, "nextWordAndCat", ["0", tag, "1", nextWord, "1", nextTag]));
168
+ }
169
+ */
170
+ };
171
+
172
+ module.exports = POS_Element;
@@ -0,0 +1,141 @@
1
+ /*
2
+ Sample space of observed events
3
+ Copyright (C) 2018 Hugo W.L. ter Doest
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ var Context = require('./Context');
20
+
21
+ var fs = require('fs');
22
+
23
+ function Sample(elements) {
24
+ this.frequencyOfContext = {};
25
+ this.frequency = {};
26
+ this.classes = [];
27
+ if (elements) {
28
+ this.elements = elements;
29
+ this.analyse();
30
+ }
31
+ else {
32
+ this.elements = [];
33
+ }
34
+ }
35
+
36
+ // Extracts classes and frequencies
37
+ Sample.prototype.analyse = function() {
38
+ var that = this;
39
+ this.elements.forEach(function(x) {
40
+ if (this.classes.indexOf(x.a) === -1) {
41
+ this.classes.push(x.a);
42
+ }
43
+ if (!that.frequencyOfContext[x.b.toString()]) {
44
+ that.frequencyOfContext[x.b.toString()] = 0;
45
+ }
46
+ that.frequencyOfContext[x.b.toString()]++;
47
+ if (!that.frequency[x.toString()]) {
48
+ that.frequency[x.toString()] = 0;
49
+ }
50
+ that.frequency[x.toString()]++;
51
+ });
52
+ };
53
+
54
+ Sample.prototype.addElement = function(x) {
55
+ this.elements.push(x);
56
+ // Update frequencies
57
+ if (!this.frequencyOfContext[x.b.toString()]) {
58
+ this.frequencyOfContext[x.b.toString()] = 0;
59
+ }
60
+ this.frequencyOfContext[x.b.toString()]++;
61
+ if (!this.frequency[x.toString()]) {
62
+ this.frequency[x.toString()] = 0;
63
+ }
64
+ this.frequency[x.toString()]++;
65
+ // Update classes
66
+ if (this.classes.indexOf(x.a) === -1) {
67
+ this.classes.push(x.a);
68
+ }
69
+ };
70
+
71
+ Sample.prototype.observedProbabilityOfContext = function(context) {
72
+ if (this.frequencyOfContext[context.toString()]) {
73
+ return this.frequencyOfContext[context.toString()] / this.elements.length;
74
+ }
75
+ else {
76
+ return 0;
77
+ }
78
+ };
79
+
80
+ Sample.prototype.observedProbability = function(x) {
81
+ if (this.frequency[x.toString()]) {
82
+ return this.frequency[x.toString()] / this.elements.length;
83
+ }
84
+ else {
85
+ return 0;
86
+ }
87
+ };
88
+
89
+ Sample.prototype.size = function() {
90
+ return this.elements.length;
91
+ };
92
+
93
+ Sample.prototype.getClasses = function() {
94
+ return this.classes;
95
+ };
96
+
97
+ Sample.prototype.generateFeatures = function(featureSet) {
98
+ this.elements.forEach(function(x) {
99
+ x.generateFeatures(featureSet);
100
+ });
101
+ };
102
+
103
+ Sample.prototype.save = function(filename, callback) {
104
+ var sample = this;
105
+ var data = JSON.stringify(this, null, 2);
106
+ fs.writeFile(filename, data, 'utf8', function(err) {
107
+ //console.log('Sample written')
108
+ if(callback) {
109
+ callback(err, err ? null : sample);
110
+ }
111
+ });
112
+ };
113
+
114
+ // Loads a sample from file and revives the right classes, i.e. Sample and
115
+ // Element classes.
116
+ Sample.prototype.load = function(filename, elementClass, callback) {
117
+ fs.readFile(filename, 'utf8', function(err, data) {
118
+
119
+ if(!err) {
120
+ var sampleData = JSON.parse(data);
121
+ var sample = new Sample();
122
+ sampleData.elements.forEach(function(elementData) {
123
+ var elt = new elementClass(elementData.a, new Context(elementData.b.data));
124
+ sample.addElement(elt);
125
+ });
126
+ if (!sample.frequency || !sample.frequencyOfContext) {
127
+ sample.analyse();
128
+ }
129
+ if (callback) {
130
+ callback(err, sample);
131
+ }
132
+ }
133
+ else {
134
+ if (callback) {
135
+ callback(err);
136
+ }
137
+ }
138
+ });
139
+ };
140
+
141
+ module.exports = Sample;
@@ -0,0 +1,49 @@
1
+ /*
2
+ Simple Example Element class
3
+ Copyright (C) 2018 Hugo W.L. ter Doest
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ var util = require('util');
20
+
21
+ var Element = require('../Element');
22
+ var Feature = require('../Feature');
23
+
24
+ function SE_Element(a, b) {
25
+ SE_Element.super_.call(this, a, b);
26
+ }
27
+
28
+ util.inherits(SE_Element, Element);
29
+
30
+ SE_Element.prototype.generateFeatures = function(featureSet) {
31
+
32
+ function isZero(x) {
33
+ if ((x.a === "x") && (x.b.data === "0")) {
34
+ return 1;
35
+ }
36
+ return 0;
37
+ }
38
+ featureSet.addFeature(new Feature(isZero, "isZero", ["0"]));
39
+
40
+ function isOne(x) {
41
+ if ((x.a === "y") && (x.b.data === "1")) {
42
+ return 1;
43
+ }
44
+ return 0;
45
+ }
46
+ featureSet.addFeature(new Feature(isOne, "isOne", ["1"]));
47
+ };
48
+
49
+ module.exports = SE_Element;
@@ -0,0 +1,52 @@
1
+ /*
2
+ Copyright (c) 2018, Shane Caldwell, Hugo ter Doest
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in
12
+ all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ THE SOFTWARE.
21
+ */
22
+
23
+ // Computes the Hamming distance between two string -- intrepreted from:
24
+ // https://en.wikipedia.org/wiki/Hamming_distance
25
+ // s1 is the first string to compare
26
+ // s2 is the second string to compare
27
+ // Strings should have equal length
28
+ function HammingDistance(s1, s2, ignoreCase) {
29
+ // Return -1 if one of the parameters is not a string
30
+ if (typeof(s1) != "string" || typeof(s2) != "string") {
31
+ return -1;
32
+ }
33
+ // Return -1 the lengths of the strings differ
34
+ if (s1.length != s2.length) {
35
+ return -1;
36
+ }
37
+
38
+ if (ignoreCase) {
39
+ s1 = s1.toLowerCase();
40
+ s2 = s2.toLowerCase();
41
+ }
42
+
43
+ var diffs = 0;
44
+ for (var i = 0; i < s1.length; i++) {
45
+ if (s1[i] != s2[i]) {
46
+ diffs++;
47
+ }
48
+ }
49
+ return diffs;
50
+ }
51
+
52
+ module.exports = HammingDistance;
@@ -35,8 +35,6 @@ function distance(s1, s2) {
35
35
  return 0;
36
36
  }
37
37
 
38
- s1 = s1.toLowerCase(), s2 = s2.toLowerCase();
39
-
40
38
  var matchWindow = (Math.floor(Math.max(s1.length, s2.length) / 2.0)) - 1;
41
39
  var matches1 = new Array(s1.length);
42
40
  var matches2 = new Array(s2.length);
@@ -83,6 +81,7 @@ function distance(s1, s2) {
83
81
  k++;
84
82
  }
85
83
  if (s1[i] !== s2[k]) { // if the characters don't match, increase transposition
84
+ // HtD: t is always less than the number of matches m, because transpositions are a subset of matches
86
85
  t++;
87
86
  }
88
87
  k++; // iterate k index normally
@@ -91,7 +90,8 @@ function distance(s1, s2) {
91
90
  // transpositions divided by 2
92
91
  t = t / 2.0;
93
92
 
94
- return ((m / s1.length) + (m / s2.length) + ((m - t) / m)) / 3.0;
93
+ return ((m / s1.length) + (m / s2.length) + ((m - t) / m)) / 3.0; // HtD: therefore, m - t > 0, and m - t < m
94
+ // HtD: => return value is between 0 and 1
95
95
  }
96
96
 
97
97
  // Computes the Winkler distance between two string -- intrepreted from:
@@ -99,19 +99,29 @@ function distance(s1, s2) {
99
99
  // s1 is the first string to compare
100
100
  // s2 is the second string to compare
101
101
  // dj is the Jaro Distance (if you've already computed it), leave blank and the method handles it
102
- function JaroWinklerDistance(s1, s2, dj) {
102
+ // ignoreCase: if true strings are first converted to lower case before comparison
103
+ function JaroWinklerDistance(s1, s2, dj, ignoreCase) {
103
104
  if (s1 === s2) {
104
105
  return 1;
105
106
  } else {
106
- var jaro = (typeof(dj) === 'undefined') ? distance(s1,s2) : dj;
107
+ if (ignoreCase) {
108
+ s1 = s1.toLowerCase();
109
+ s2 = s2.toLowerCase();
110
+ }
111
+
112
+ //console.log(news1);
113
+ //console.log(news2);
114
+
115
+ var jaro = (typeof(dj) === 'undefined') ? distance(s1, s2) : dj;
107
116
  var p = 0.1; // default scaling factor
108
117
  var l = 0 // length of the matching prefix
109
118
  while(s1[l] === s2[l] && l < 4) {
110
119
  l++;
111
120
  }
112
121
 
122
+ // HtD: 1 - jaro >= 0
113
123
  return jaro + l * p * (1 - jaro);
114
124
  }
115
125
  }
116
- module.exports = JaroWinklerDistance;
117
126
 
127
+ module.exports = JaroWinklerDistance;
@@ -29,40 +29,193 @@ THE SOFTWARE.
29
29
  * Algorithm based from Speech and Language Processing - Daniel Jurafsky and James H. Martin.
30
30
  */
31
31
 
32
- function LevenshteinDistance (source, target, options) {
33
- options = options || {};
32
+ var _ = require('underscore');
33
+
34
+ // Walk the path back from the matchEnd to the beginning of the match.
35
+ // Do this by traversing the distanceMatrix as you would a linked list,
36
+ // following going from cell child to parent until reach row 0.
37
+ function _getMatchStart(distanceMatrix, matchEnd, sourceLength) {
38
+ var row = sourceLength;
39
+ var column = matchEnd;
40
+ var tmpRow;
41
+ var tmpColumn;
42
+
43
+ // match will be empty string
44
+ if (matchEnd === 0) { return 0; }
45
+
46
+ while(row > 1 && column > 1) {
47
+ tmpRow = row;
48
+ tmpColumn = column;
49
+ row = distanceMatrix[tmpRow][tmpColumn].parentCell.row;
50
+ column = distanceMatrix[tmpRow][tmpColumn].parentCell.column;
51
+ }
52
+
53
+ return column-1;
54
+ }
55
+
56
+ function getMinCostSubstring(distanceMatrix, source, target) {
57
+ var sourceLength = source.length;
58
+ var targetLength = target.length;
59
+ var minDistance = sourceLength + targetLength;
60
+ var matchEnd = targetLength;
61
+
62
+ // Find minimum value in last row of the cost matrix. This cell marks the
63
+ // end of the match string.
64
+ for (var column = 0; column <= targetLength; column++) {
65
+ if (minDistance > distanceMatrix[sourceLength][column].cost) {
66
+ minDistance = distanceMatrix[sourceLength][column].cost;
67
+ matchEnd = column;
68
+ }
69
+ }
70
+
71
+ matchStart = _getMatchStart(distanceMatrix, matchEnd, sourceLength);
72
+ return {substring: target.slice(matchStart, matchEnd), distance: minDistance};
73
+ }
74
+
75
+ /*
76
+ * Returns the Damerau-Levenshtein distance between strings. Counts the distance
77
+ * between two strings by returning the number of edit operations required to
78
+ * convert `source` into `target`.
79
+ *
80
+ * Valid edit operations are:
81
+ * - transposition, insertion, deletion, and substitution
82
+ *
83
+ * Options:
84
+ * insertion_cost: (default: 1)
85
+ * deletion_cost: number (default: 1)
86
+ * substitution_cost: number (default: 1)
87
+ * transposition_cost: number (default: 1)
88
+ * search: boolean (default: false)
89
+ * restricted: boolean (default: false)
90
+ */
91
+ function DamerauLevenshteinDistance(source, target, options) {
92
+ var damLevOptions = _.extend(
93
+ { transposition_cost: 1, restricted: false },
94
+ options || {},
95
+ { damerau: true }
96
+ );
97
+ return levenshteinDistance(source, target, damLevOptions);
98
+ }
99
+
100
+ function LevenshteinDistance(source, target, options) {
101
+ var levOptions = _.extend({}, options || {}, { damerau: false });
102
+ return levenshteinDistance(source, target, levOptions);
103
+ }
104
+
105
+
106
+ function levenshteinDistance (source, target, options) {
34
107
  if(isNaN(options.insertion_cost)) options.insertion_cost = 1;
35
108
  if(isNaN(options.deletion_cost)) options.deletion_cost = 1;
36
109
  if(isNaN(options.substitution_cost)) options.substitution_cost = 1;
37
110
 
111
+ if(typeof options.search !== 'boolean') options.search = false;
112
+
113
+ var isUnrestrictedDamerau = options.damerau && !options.restricted;
114
+ var isRestrictedDamerau = options.damerau && options.restricted;
115
+
116
+ if (isUnrestrictedDamerau) {
117
+ var lastRowMap = {};
118
+ }
119
+
38
120
  var sourceLength = source.length;
39
121
  var targetLength = target.length;
40
- var distanceMatrix = [[0]];
122
+ var distanceMatrix = [[{cost: 0}]]; //the root, has no parent cell
41
123
 
42
124
  for (var row = 1; row <= sourceLength; row++) {
43
125
  distanceMatrix[row] = [];
44
- distanceMatrix[row][0] = distanceMatrix[row-1][0] + options.deletion_cost;
126
+ distanceMatrix[row][0] = {cost: distanceMatrix[row-1][0].cost + options.deletion_cost, parentCell: {row: row-1, column: 0}};
45
127
  }
46
128
 
47
129
  for (var column = 1; column <= targetLength; column++) {
48
- distanceMatrix[0][column] = distanceMatrix[0][column-1] + options.insertion_cost;
130
+ if (options.search) {
131
+ distanceMatrix[0][column] = {cost: 0};
132
+ } else {
133
+ distanceMatrix[0][column] = {cost: distanceMatrix[0][column-1].cost + options.insertion_cost, parentCell: {row: 0, column: column-1}};
134
+ }
49
135
  }
50
136
 
51
137
  for (var row = 1; row <= sourceLength; row++) {
138
+ if (isUnrestrictedDamerau) {
139
+ var lastColMatch = null;
140
+ }
52
141
  for (var column = 1; column <= targetLength; column++) {
53
- var costToInsert = distanceMatrix[row][column-1] + options.insertion_cost;
54
- var costToDelete = distanceMatrix[row-1][column] + options.deletion_cost;
142
+ var costToInsert = distanceMatrix[row][column-1].cost + options.insertion_cost;
143
+ var costToDelete = distanceMatrix[row-1][column].cost + options.deletion_cost;
55
144
 
56
145
  var sourceElement = source[row-1];
57
146
  var targetElement = target[column-1];
58
- var costToSubstitute = distanceMatrix[row-1][column-1];
147
+ var costToSubstitute = distanceMatrix[row-1][column-1].cost;
59
148
  if (sourceElement !== targetElement) {
60
149
  costToSubstitute = costToSubstitute + options.substitution_cost;
61
150
  }
62
- distanceMatrix[row][column] = Math.min(costToInsert, costToDelete, costToSubstitute);
151
+
152
+ var possibleParents = [
153
+ {cost: costToInsert, coordinates: {row: row, column: column-1}},
154
+ {cost: costToDelete, coordinates: {row: row-1, column: column}},
155
+ {cost: costToSubstitute, coordinates: {row: row-1, column: column-1}}
156
+ ];
157
+
158
+ // We can add damerau to the possibleParents if the current
159
+ // target-letter has been encountered in our lastRowMap,
160
+ // and if there exists a previous column in this row where the
161
+ // row & column letters matched
162
+ var canDamerau = isUnrestrictedDamerau
163
+ && row > 1 && column > 1
164
+ && lastColMatch
165
+ && targetElement in lastRowMap;
166
+
167
+ if (canDamerau) {
168
+ var lastRowMatch = lastRowMap[targetElement];
169
+ var costBeforeTransposition =
170
+ distanceMatrix[lastRowMatch - 1][lastColMatch - 1].cost;
171
+ var costToTranspose = costBeforeTransposition
172
+ + ((row - lastRowMatch - 1) * options.deletion_cost)
173
+ + ((column - lastColMatch - 1) * options.insertion_cost)
174
+ + options.transposition_cost;
175
+ possibleParents.push({
176
+ cost: costToTranspose,
177
+ coordinates: {
178
+ row: lastRowMatch - 1,
179
+ column: lastColMatch - 1,
180
+ },
181
+ });
182
+ }
183
+ // Source and target chars are 1-indexed in the distanceMatrix so previous
184
+ // source/target element is (col/row - 2)
185
+ var canDoRestrictedDamerau = isRestrictedDamerau
186
+ && row > 1 && column > 1
187
+ && sourceElement === target[column - 2]
188
+ && source[row - 2] === targetElement;
189
+
190
+ if (canDoRestrictedDamerau) {
191
+ var costBeforeTransposition = distanceMatrix[row - 2][column - 2].cost;
192
+ possibleParents.push({
193
+ cost: costBeforeTransposition + options.transposition_cost,
194
+ coordinates: { row: row - 2, column: column - 2 },
195
+ });
196
+ }
197
+
198
+ var minCostParent = _.min(possibleParents, function(p) { return p.cost; });
199
+
200
+ distanceMatrix[row][column] = {cost: minCostParent.cost, parentCell: minCostParent.coordinates};
201
+
202
+ if (isUnrestrictedDamerau) {
203
+ lastRowMap[sourceElement] = row;
204
+ if (sourceElement === targetElement) {
205
+ lastColMatch = column;
206
+ }
207
+ }
63
208
  }
64
209
  }
65
- return distanceMatrix[sourceLength][targetLength];
210
+
211
+ if (!options.search) {
212
+ return distanceMatrix[sourceLength][targetLength].cost;
213
+ }
214
+
215
+ return getMinCostSubstring(distanceMatrix, source, target);
66
216
  }
67
217
 
68
- module.exports = LevenshteinDistance;
218
+ module.exports = {
219
+ LevenshteinDistance: LevenshteinDistance,
220
+ DamerauLevenshteinDistance: DamerauLevenshteinDistance,
221
+ };