-
Notifications
You must be signed in to change notification settings - Fork 7
/
NumberUtils.js
206 lines (182 loc) · 6.73 KB
/
NumberUtils.js
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/**
* NumberUtils.js
*
* Defines the class NumberRange, as well as other functions for displaying,
* and creating filters on, numbers.
*
* @author Yaron Koren
*/
var gBucketsPerFilter = 6;
// Copied from http://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
function numberWithCommas(x) {
var parts = x.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
}
function NumberRange( lowNumber, highNumber ) {
this.lowNumber = lowNumber;
this.highNumber = highNumber;
}
NumberRange.fromString = function( filterText ) {
var numberRange = new NumberRange();
filterText = String(filterText);
var numbers = filterText.split(' - ');
if ( numbers.length == 2 ) {
numberRange.lowNumber = parseFloat(numbers[0]);
numberRange.highNumber = parseFloat(numbers[1]);
} else {
numberRange.lowNumber = parseFloat(filterText);
numberRange.highNumber = null;
}
return numberRange;
}
/**
* Gets the non-display string for this number range.
*/
NumberRange.prototype.toString = function() {
if ( this.highNumber == null ) {
return this.lowNumber;
} else {
return this.lowNumber + " - " + this.highNumber;
}
}
/**
* Gets the string to be displayed on the screen for this number range.
*/
NumberRange.prototype.toDisplayString = function() {
if ( this.highNumber == null ) {
return numberWithCommas( this.lowNumber );
} else {
return numberWithCommas( this.lowNumber ) + " - " + numberWithCommas( this.highNumber );
}
}
function getNearestNiceNumber( num, previousNum, nextNum ) {
if ( previousNum == null ) {
var smallestDifference = nextNum - num;
} else if ( nextNum == null ) {
var smallestDifference = num - previousNum;
} else {
var smallestDifference = Math.min( num - previousNum, nextNum - num );
}
var base10LogOfDifference = Math.log(smallestDifference) / Math.LN10;
var significantFigureOfDifference = Math.floor( base10LogOfDifference );
var powerOf10InCorrectPlace = Math.pow(10, Math.floor(base10LogOfDifference));
var significantDigitsOnly = Math.round( num / powerOf10InCorrectPlace );
var niceNumber = significantDigitsOnly * powerOf10InCorrectPlace;
// Special handling if it's the first or last number in the series -
// we have to make sure that the "nice" equivalent is on the right
// "side" of the number.
// That's especially true for the last number -
// it has to be greater, not just equal to, because of the way
// number filtering works.
// ...or does it??
if ( previousNum == null && niceNumber > num ) {
niceNumber -= powerOf10InCorrectPlace;
}
if ( nextNum == null && niceNumber < num ) {
niceNumber += powerOf10InCorrectPlace;
}
// Now, we have to turn it into a string, so that the resulting
// number doesn't end with something like ".000000001" due to
// floating-point arithmetic.
var numDecimalPlaces = Math.max( 0, 0 - significantFigureOfDifference );
return niceNumber.toFixed( numDecimalPlaces );
}
/**
* Each of these filter values will be a single number, as opposed to
* a range.
*/
function generateIndividualFilterValuesFromNumbers( uniqueValues ) {
// Unfortunately, object keys aren't necessarily cycled through
// in the correct order - put them in an array, so that they can
// be sorted.
var uniqueValuesArray = [];
for ( uniqueValue in uniqueValues ) {
uniqueValuesArray.push( uniqueValue );
}
// Sort numerically, not alphabetically.
uniqueValuesArray.sort( function(a,b) { return a - b; } );
var propertyValues = [];
for ( i = 0; i < uniqueValuesArray.length; i++ ) {
var uniqueValue = uniqueValuesArray[i];
var curBucket = {};
curBucket['filterName'] = uniqueValue;
curBucket['numValues'] = uniqueValues[uniqueValue];
propertyValues.push( curBucket );
}
return propertyValues;
}
function generateFilterValuesFromNumbers( numberArray ) {
var numNumbers = numberArray.length;
// First, find the number of unique values - if it's the value of
// gBucketsPerFilter, or fewer, just display each one as its own
// bucket.
var numUniqueValues = 0;
var uniqueValues = {};
for ( i = 0; i < numNumbers; i++ ) {
var curNumber = numberArray[i];
if ( !uniqueValues.hasOwnProperty(curNumber) ) {
uniqueValues[curNumber] = 1;
numUniqueValues++;
if ( numUniqueValues > gBucketsPerFilter ) continue;
} else {
// We do this now to save time on the next step,
// if we're creating individual filter values.
uniqueValues[curNumber]++;
}
}
if ( numUniqueValues <= gBucketsPerFilter ) {
return generateIndividualFilterValuesFromNumbers( uniqueValues );
}
var propertyValues = [];
var separatorValue = numberArray[0];
var startIndexOfBucket = 0;
var endIndexOfBucket;
// Make sure there are at least, on average, five numbers per bucket.
// HACK - add 3 to the number so that we don't end up with just one
// bucket ( 7 + 3 / 5 = 2).
var numBuckets = Math.min( gBucketsPerFilter, Math.floor( (numNumbers + 3) / 5 ) );
var bucketSeparators = [];
bucketSeparators.push( numberArray[0] );
for (i = 1; i < numBuckets; i++) {
separatorIndex = Math.floor( numNumbers * i / numBuckets ) - 1;
previousSeparatorValue = separatorValue;
separatorValue = numberArray[separatorIndex];
if ( separatorValue == previousSeparatorValue ) {
continue;
}
bucketSeparators.push( separatorValue );
}
bucketSeparators.push( Math.ceil( numberArray[numberArray.length - 1] ) );
// Get the closest "nice" (few significant digits) number for each of
// the bucket separators, with the number of significant digits
// required based on their proximity to their neighbors.
// The first and last separators need special handling.
bucketSeparators[0] = getNearestNiceNumber( bucketSeparators[0], null, bucketSeparators[1] );
for (i = 1; i < bucketSeparators.length - 1; i++) {
bucketSeparators[i] = getNearestNiceNumber( bucketSeparators[i], bucketSeparators[i - 1], bucketSeparators[i + 1] );
}
bucketSeparators[bucketSeparators.length - 1] = getNearestNiceNumber( bucketSeparators[bucketSeparators.length - 1], bucketSeparators[bucketSeparators.length - 2], null );
var oldSeparatorValue = bucketSeparators[0];
var separatorValue;
for ( i = 1; i < bucketSeparators.length; i++ ) {
separatorValue = bucketSeparators[i];
var curBucket = {};
curBucket['numValues'] = 0;
var curFilter = new NumberRange( oldSeparatorValue, separatorValue );
curBucket['filterName'] = curFilter.toString();
propertyValues.push(curBucket);
oldSeparatorValue = separatorValue;
}
var curSeparator = 0;
for (i = 0; i < numberArray.length; i++) {
if ( curSeparator < propertyValues.length - 1 ) {
var curNumber = numberArray[i];
while ( curNumber >= bucketSeparators[curSeparator + 1] ) {
curSeparator++;
}
}
propertyValues[curSeparator]['numValues']++;
}
return propertyValues;
}