@flourish/interpreter
Advanced tools
Comparing version 0.2.1 to 1.0.0
{ | ||
"name": "@flourish/interpreter", | ||
"version": "0.2.1", | ||
"version": "1.0.0", | ||
"description": "Does a best guess at the type of data supplied", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
@@ -0,1 +1,9 @@ | ||
# 1.0.0 | ||
* Made createInterpreter the default export. | ||
* Optionally specify allowed types in interpreter creation. | ||
* Methods for specifying how many failures are allowed and how many different failing values. | ||
* Optionally (on by default) sort by descending failure fraction (with string always last). | ||
* Max number of array elements to interpret now set at method of interpreter. | ||
* Drop coordinate types. Intend to resurrect as properties or methods of number types in the future. | ||
# 0.2.1 | ||
@@ -13,2 +21,2 @@ * Fix 1PM format (previously it would render as 01 PM which was not the intended behaviour) | ||
* Improve date handling | ||
* Add longitude and latitude types | ||
* Add coordinate types |
@@ -8,3 +8,3 @@ import nodeResolve from "rollup-plugin-node-resolve"; | ||
format: "umd", | ||
name: "interpreter" | ||
name: "createInterpreter" | ||
}, | ||
@@ -11,0 +11,0 @@ plugins: [ |
121
src/index.js
@@ -1,36 +0,111 @@ | ||
import { datetime_interpreters } from "./types/datetime"; | ||
import { number_interpreters } from "./types/number"; | ||
import { string_interpreters } from "./types/string"; | ||
import { coordinate_interpreters } from "./types/coordinate"; | ||
import { datetime_interpretations } from "./types/datetime"; | ||
import { number_interpretations } from "./types/number"; | ||
import { string_interpretation } from "./types/string"; | ||
function createParserTest(val) { return function(interpreter) { return interpreter.test(val); }; } | ||
var INTERPRETATION_OPTIONS = Object.freeze({ | ||
datetime: datetime_interpretations, | ||
number: number_interpretations, | ||
}); | ||
function _createAccessorFunction(arr, accessor) { | ||
if (accessor === undefined) return function(index) { return arr[index]; }; | ||
if (typeof accessor === "function") return function(index) { return accessor(arr[index], index); }; | ||
return function(index) { return arr[index]["" + accessor]; }; | ||
var DEFAULT_INTERPRETATIONS_ARRAY = Object.freeze([ "datetime", "number", "string" ]); | ||
var DEFAULT_OPTIONS = Object.freeze({ | ||
n_max: 250, | ||
n_failing_values: 0, | ||
failure_fraction: 5 / 100, | ||
sort: true | ||
}); | ||
var OPTION_KEYS = Object.freeze(Object.keys(DEFAULT_OPTIONS)); | ||
function snakeToCamel(snake_string) { | ||
return snake_string.replace(/_(\w)/g, function(match, capture) { | ||
return capture.toUpperCase(); | ||
}); | ||
} | ||
function createInterpreter(n_max) { | ||
n_max = n_max > 0 ? n_max : 1000; | ||
function noSort(a, b) { return a.index - b.index; } | ||
function sortBySuccess(a, b) { return b.n_success - a.n_success || noSort(a, b); } | ||
return function(arr, accessor) { | ||
if (!Array.isArray(arr)) return undefined; | ||
if (!arr.length) return string_interpreters.slice(); | ||
var n = Math.min(n_max, arr.length); | ||
var getVal = _createAccessorFunction(arr, accessor); | ||
var interpreters = datetime_interpreters.concat(number_interpreters, coordinate_interpreters); | ||
function createAccessorFunction(accessor) { | ||
if (accessor === undefined) return function(value) { return value; }; | ||
if (typeof accessor === "function") return function(value, index) { return accessor(value, index); }; | ||
return function(value) { return value["" + accessor]; }; | ||
} | ||
for (var i = 0; i < n && interpreters.length; i++) { | ||
var val = getVal(i); | ||
if (val) interpreters = interpreters.filter(createParserTest(val)); | ||
} | ||
return interpreters.concat(string_interpreters); | ||
function createInterpreter(interpretations_array) { | ||
if (!interpretations_array) interpretations_array = DEFAULT_INTERPRETATIONS_ARRAY; | ||
else if (!Array.isArray(interpretations_array)) interpretations_array = [interpretations_array]; | ||
var interpretations = interpretations_array.reduce(function(arr, interp_string) { | ||
var interps = INTERPRETATION_OPTIONS[interp_string]; | ||
if (interps) Array.prototype.push.apply(arr, interps); | ||
return arr; | ||
}, []); | ||
var include_string = interpretations_array.indexOf("string" !== -1); | ||
var options = OPTION_KEYS.reduce(function(obj, key) { | ||
obj[key] = DEFAULT_OPTIONS[key]; | ||
return obj; | ||
}, {}); | ||
var interpreter = function(input_array, accessor) { | ||
accessor = createAccessorFunction(accessor); | ||
var data = input_array.map(accessor).filter(function(x) { return x; }); | ||
var n = Math.min(options.n_max, data.length); | ||
var n_max_failure = Math.floor(n * options.failure_fraction); | ||
var n_failing_values = options.n_failing_values; | ||
var sortMethod = options.sort ? sortBySuccess : noSort; | ||
var valid_interpreters = interpretations.slice() | ||
.reduce(function(keep, interp, index) { | ||
var n_fail = i = 0; | ||
var failing_values = []; | ||
var complete_failure = false; | ||
for (var i = 0; i < n; i++) { | ||
var val = data[i]; | ||
var is_valid = interp.test(val); | ||
if (is_valid) continue; | ||
if (++n_fail > n_max_failure) complete_failure = true; | ||
else if (failing_values.indexOf(val) === -1) { | ||
failing_values.push(val); | ||
if (failing_values.length > n_failing_values) complete_failure = true; | ||
} | ||
if (complete_failure) break; | ||
} | ||
if (!complete_failure) keep.push({ interp: interp, n_success: n - n_fail, index: index }); | ||
return keep; | ||
}, []) | ||
.sort(sortMethod) | ||
.map(function(valid) { | ||
return valid.interp; | ||
}); | ||
if (include_string) valid_interpreters.push(string_interpretation); | ||
return valid_interpreters; | ||
}; | ||
OPTION_KEYS.forEach(function(option) { | ||
interpreter[snakeToCamel(option)] = function(value) { | ||
if (value === undefined) return options[option]; | ||
options[option] = value; | ||
return interpreter; | ||
}; | ||
}); | ||
return interpreter; | ||
} | ||
export { createInterpreter, _createAccessorFunction }; | ||
createInterpreter._createAccessorFunction = createAccessorFunction; | ||
export default createInterpreter; | ||
import { timeFormat, timeParse } from "d3-time-format"; | ||
function createDatetimeInterpreter(format_string, secondaryTest) { | ||
function createDatetimeInterpretation(format_string, secondaryTest) { | ||
var parser = timeParse(format_string); | ||
@@ -23,32 +24,33 @@ var formatter = timeFormat(format_string); | ||
var datetime_interpreters = Object.freeze([ | ||
createDatetimeInterpreter("%Y-%m-%dT%H:%M:%S.%LZ"), | ||
createDatetimeInterpreter("%Y-%m-%d"), | ||
createDatetimeInterpreter("%m/%d/%Y", function(str) { | ||
var datetime_interpretations = Object.freeze([ | ||
createDatetimeInterpretation("%Y-%m-%dT%H:%M:%S.%LZ"), | ||
createDatetimeInterpretation("%Y-%m-%d"), | ||
createDatetimeInterpretation("%m/%d/%Y", function(str) { | ||
var arr = str.split("/").map(parseFloat); | ||
return (arr[0] > 0 && arr[0] <= 12) && (arr[1] > 0 && arr[1] <= 31) && (!isNaN(arr[2])); | ||
}), | ||
createDatetimeInterpreter("%d/%m/%Y", function(str) { | ||
createDatetimeInterpretation("%d/%m/%Y", function(str) { | ||
var arr = str.split("/").map(parseFloat); | ||
return (arr[0] > 0 && arr[0] <= 31) && (arr[1] > 0 && arr[1] <= 12) && (!isNaN(arr[2])); | ||
}), | ||
createDatetimeInterpreter("%d-%b-%y"), | ||
createDatetimeInterpreter("%m/%y"), | ||
createDatetimeInterpreter("%m/%Y"), | ||
createDatetimeInterpreter("%b %Y"), | ||
createDatetimeInterpreter("%B %d"), | ||
createDatetimeInterpreter("%d %b"), | ||
createDatetimeInterpreter("%Y", function(str) { | ||
createDatetimeInterpretation("%d-%b-%y"), | ||
createDatetimeInterpretation("%m/%y"), | ||
createDatetimeInterpretation("%m/%Y"), | ||
createDatetimeInterpretation("%b %Y"), | ||
createDatetimeInterpretation("%B %d"), | ||
createDatetimeInterpretation("%d %b"), | ||
createDatetimeInterpretation("%Y", function(str) { | ||
var val = parseFloat(str); | ||
return val > 1499 && val < 2200; | ||
}), | ||
createDatetimeInterpreter("%B"), | ||
createDatetimeInterpreter("%b"), | ||
createDatetimeInterpreter("%X"), | ||
createDatetimeInterpreter("%I:%M %p"), | ||
createDatetimeInterpreter("%-I%p"), | ||
createDatetimeInterpreter("%H:%M") | ||
createDatetimeInterpretation("%B"), | ||
createDatetimeInterpretation("%b"), | ||
createDatetimeInterpretation("%X"), | ||
createDatetimeInterpretation("%I:%M %p"), | ||
createDatetimeInterpretation("%-I%p"), | ||
createDatetimeInterpretation("%H:%M") | ||
]); | ||
export { datetime_interpreters }; | ||
export { datetime_interpretations }; |
@@ -79,3 +79,3 @@ // https://stackoverflow.com/a/16148273 | ||
parse: space_point.parse, | ||
description: "Optional comma separator, point decimal mark", | ||
description: "Optional space separator, point decimal mark", | ||
thousand_separator: ",", | ||
@@ -86,4 +86,5 @@ decimal_mark: "." | ||
var number_interpreters = [ | ||
var number_interpretations = Object.freeze([ | ||
comma_point, | ||
space_point, | ||
point_comma, | ||
@@ -97,12 +98,10 @@ space_comma, | ||
opt_space_point | ||
]; | ||
]); | ||
number_interpreters.forEach(function(interpreter) { | ||
interpreter.type = "number"; | ||
Object.freeze(interpreter); | ||
number_interpretations.forEach(function(interp) { | ||
interp.type = "number"; | ||
Object.freeze(interp); | ||
}); | ||
Object.freeze(number_interpreters); | ||
export { number_interpreters }; | ||
export { number_interpretations }; |
@@ -1,12 +0,10 @@ | ||
var string_interpreters = Object.freeze([ | ||
Object.freeze({ | ||
test: function() { return true; }, | ||
parse: function(str) { return str; }, | ||
type: "string", | ||
stringToString: function(str) { return str; }, | ||
description: "Arbitrary string" | ||
}) | ||
]); | ||
var string_interpretation = Object.freeze({ | ||
test: function() { return true; }, | ||
parse: function(str) { return str; }, | ||
type: "string", | ||
stringToString: function(str) { return str; }, | ||
description: "Arbitrary string" | ||
}); | ||
export { string_interpreters }; | ||
export { string_interpretation }; |
const { expect } = require("chai"); | ||
const { createInterpreter, _createAccessorFunction } = require("../interpreter"); | ||
const createInterpreter = require("../interpreter"); | ||
const _createAccessorFunction = createInterpreter._createAccessorFunction; | ||
const string_data = [ "a", "b", "c", "d"]; | ||
@@ -10,85 +12,100 @@ const object_data = [ { letter: "a" }, { letter: "b" }, { letter: "c" }, { letter: "d" }]; | ||
describe("_createAccessorFunction", () => { | ||
describe("when an array of strings is the first argument", () => { | ||
describe("and there is no second argument", () => { | ||
it("should return function that can be used to create a copy of the array", () => { | ||
const f = _createAccessorFunction(string_data); | ||
expect(string_data.map((d,i) => f(i))).to.deep.equal(string_data); | ||
}); | ||
describe("when no accessor argument is passed in", () => { | ||
it("should return function that can be used to create a copy of the array", () => { | ||
const f = _createAccessorFunction(); | ||
expect(string_data.map(f)).to.deep.equal(string_data); | ||
}); | ||
}); | ||
describe("when an array of objects is the first argument", () => { | ||
describe("and the second argument specifies a key in those objects", () => { | ||
it("should return function that can be used to create an array of values corresponding to that key", () => { | ||
const f = _createAccessorFunction(object_data, "letter"); | ||
expect(object_data.map((d,i) => f(i))).to.deep.equal(string_data); | ||
}); | ||
describe("when a string accessor is specified", () => { | ||
it("should return function that can be used to create an array of values from an array of objects with that key", () => { | ||
const f = _createAccessorFunction("letter"); | ||
expect(object_data.map(f)).to.deep.equal(string_data); | ||
}); | ||
}); | ||
describe("and the second argument specifies a function that returns a property of each object", () => { | ||
it("should return function that can be used to create an array of values corresponding to that key", () => { | ||
const f = _createAccessorFunction(object_data, d => d.letter); | ||
expect(object_data.map((d,i) => f(i))).to.deep.equal(string_data); | ||
}); | ||
describe("when a function accessor is specified", () => { | ||
it("should return function that behaves the same as the accessor function (with two arguments)", () => { | ||
let f = _createAccessorFunction(d => d.letter); | ||
expect(object_data.map(f)).to.deep.equal(string_data); | ||
f = _createAccessorFunction((d, i) => d[i]); | ||
expect(array_data.map(f)).to.deep.equal(string_data); | ||
}); | ||
}); | ||
describe("and the second argument is not undefined, a function, a string or a number", () => { | ||
it("should do string conversion and proceed as if that string was the accessor", () => { | ||
let f = _createAccessorFunction(object_data, null); | ||
expect(object_data.map((d,i) => f(i))).to.deep.equal([ undefined, undefined, undefined, undefined ]); | ||
var null_object_array = [ { "null": 7 } ] | ||
f = _createAccessorFunction(null_object_array, null); | ||
expect(null_object_array.map((d,i) => f(i))).to.deep.equal([ 7 ]); | ||
}); | ||
describe("when an accessor is defined that is not a function or a string", () => { | ||
it("should do string conversion and proceed as if that string was the accessor", () => { | ||
let f = _createAccessorFunction(null); | ||
expect(object_data.map(f)).to.deep.equal([ undefined, undefined, undefined, undefined ]); | ||
f = _createAccessorFunction(null); | ||
expect([ { "null": 7 } ].map(f)).to.deep.equal([ 7 ]); | ||
f = _createAccessorFunction(0); | ||
expect(array_data.map(f)).to.deep.equal([ "a", "a", "a", "a" ]); | ||
}); | ||
}); | ||
}); | ||
describe("when an array of arrays is the first argument", () => { | ||
describe("and the second argument is a number", () => { | ||
it("should return function that can be used to create an array of values corresponding to the value at that index in each array", () => { | ||
const f = _createAccessorFunction(array_data, 0); | ||
expect(array_data.map((d,i) => f(i))).to.deep.equal([ "a", "a", "a", "a" ]); | ||
}); | ||
}); | ||
describe("and the second argument is a function that uses indexes to access properties", () => { | ||
it("should return function that can be used to create an array of values corresponding to increasing indexes in each array", () => { | ||
const f = _createAccessorFunction(array_data, (d, i) => d[i]); | ||
expect(array_data.map((d,i) => f(i))).to.deep.equal(string_data); | ||
}); | ||
}); | ||
describe("interpeter", () => { | ||
it("should correctly interpret and format well-formatted datetimes", () => { | ||
const interpret = createInterpreter(); | ||
const checkMatch = function(arr, description) { | ||
const interpretation = interpret(arr)[0]; | ||
if (interpretation.type !== "datetime" || interpretation.description !== description) return false; | ||
var interpreted = arr.map(interpretation.stringToString); | ||
if (arr.length !== interpreted.length) return false; | ||
return interpreted.every((v, i) => v === arr[i]); | ||
}; | ||
const DATE_STRINGS = ["2016-12-03T08:19:56.000Z", "1927-04-03T12:25:32.000Z", "1502-10-21T14:44:28.000Z"]; | ||
expect(checkMatch(DATE_STRINGS, "%Y-%m-%dT%H:%M:%S.%LZ")).to.equal(true); | ||
expect(checkMatch(DATE_STRINGS.map(d => d.split("T")[0]), "%Y-%m-%d")).to.equal(true); | ||
expect(checkMatch(["2016-12-03", "1927-04-03", "1502-10-21"], "%Y-%m-%d")).to.equal(true); | ||
expect(checkMatch(["12/03/2016", "04/03/1927", "10/21/1502"], "%m/%d/%Y")).to.equal(true); | ||
expect(checkMatch(["03/12/2016", "03/04/1927", "21/10/1502"], "%d/%m/%Y")).to.equal(true); | ||
expect(checkMatch(["03-Dec-16", "03-Apr-27", "21-Oct-84"], "%d-%b-%y")).to.equal(true); | ||
expect(checkMatch(["12/16", "04/27", "10/84"], "%m/%y")).to.equal(true); | ||
expect(checkMatch(["12/2016", "04/1927", "10/1502"], "%m/%Y")).to.equal(true); | ||
expect(checkMatch(["Dec 2016", "Apr 1927", "Oct 1502"], "%b %Y")).to.equal(true); | ||
expect(checkMatch(["December 03", "April 12", "October 21"], "%B %d")).to.equal(true); | ||
expect(checkMatch(["03 Dec", "03 Apr", "21 Oct"], "%d %b")).to.equal(true); | ||
expect(checkMatch(["2016", "1927", "1502"], "%Y")).to.equal(true); | ||
expect(checkMatch(["December", "April", "October"], "%B")).to.equal(true); | ||
expect(checkMatch(["Dec", "Apr", "Oct"], "%b")).to.equal(true); | ||
expect(checkMatch(["8:19:56 AM", "12:25:32 PM", "2:44:28 PM"], "%X")).to.equal(true); | ||
expect(checkMatch(["08:19 AM", "12:25 PM", "02:44 PM"], "%I:%M %p")).to.equal(true); | ||
expect(checkMatch(["8AM", "12PM", "2PM"], "%-I%p")).to.equal(true); | ||
expect(checkMatch(["08:19", "12:25", "14:44"], "%H:%M")).to.equal(true); | ||
}); | ||
it("should correctly interpret consistent numbers", () => { | ||
const interpret = createInterpreter(); | ||
const checkMatch = function(arr, description) { | ||
const interpretation = interpret(arr)[0]; | ||
return interpretation.type === "number" && interpretation.description === description; | ||
}; | ||
expect(checkMatch(["-787", "1,246.83", "203", "1,340,201"], "Comma thousand separator, point decimal mark")).to.equal(true); | ||
expect(checkMatch(["-787", "1 246.83", "203", "1 340 201"], "Space thousand separator, point decimal mark")).to.equal(true); | ||
expect(checkMatch(["-787", "1246.83", "203", "1340201"], "No thousand separator, point decimal mark")).to.equal(true); | ||
expect(checkMatch(["-787", "1.246,83", "203", "1.340.201"], "Point thousand separator, comma decimal mark")).to.equal(true); | ||
expect(checkMatch(["-787", "1 246,83", "203", "1 340 201"], "Space thousand separator, comma decimal mark")).to.equal(true); | ||
expect(checkMatch(["-787", "1246,83", "203", "1340201"], "No thousand separator, comma decimal mark")).to.equal(true); | ||
expect(checkMatch(["-787", "1246.83", "203", "1,340,201"], "Optional comma separator, point decimal mark")).to.equal(true); | ||
expect(checkMatch(["-787", "1246,83", "203", "1.340.201"], "Optional point thousand separator, comma decimal mark")).to.equal(true); | ||
expect(checkMatch(["-787", "1246,83", "203", "1 340 201"], "Optional space thousand separator, comma decimal mark")).to.equal(true); | ||
expect(checkMatch(["-787", "1246.83", "203", "1 340 201"], "Optional space separator, point decimal mark")).to.equal(true); | ||
}); | ||
describe("interpeter", () => { | ||
it("should correctly interpret and format well-formatted datetimes", () => { | ||
const interpret = createInterpreter(); | ||
const checkMatch = function(arr, description) { | ||
const interpretation = interpret(arr)[0]; | ||
if (interpretation.type !== "datetime" || interpretation.description !== description) return false; | ||
var interpreted = arr.map(interpretation.stringToString); | ||
if (arr.length !== interpreted.length) return false; | ||
return interpreted.every((v, i) => v === arr[i]); | ||
}; | ||
const DATE_STRINGS = ["2016-12-03T08:19:56.000Z", "1927-04-03T12:25:32.000Z", "1502-10-21T14:44:28.000Z"]; | ||
expect(checkMatch(DATE_STRINGS, "%Y-%m-%dT%H:%M:%S.%LZ")).to.equal(true); | ||
expect(checkMatch(DATE_STRINGS.map(d => d.split("T")[0]), "%Y-%m-%d")).to.equal(true); | ||
expect(checkMatch(["2016-12-03", "1927-04-03", "1502-10-21"], "%Y-%m-%d")).to.equal(true); | ||
expect(checkMatch(["12/03/2016", "04/03/1927", "10/21/1502"], "%m/%d/%Y")).to.equal(true); | ||
expect(checkMatch(["03/12/2016", "03/04/1927", "21/10/1502"], "%d/%m/%Y")).to.equal(true); | ||
expect(checkMatch(["03-Dec-16", "03-Apr-27", "21-Oct-84"], "%d-%b-%y")).to.equal(true); | ||
expect(checkMatch(["12/16", "04/27", "10/84"], "%m/%y")).to.equal(true); | ||
expect(checkMatch(["12/2016", "04/1927", "10/1502"], "%m/%Y")).to.equal(true); | ||
expect(checkMatch(["Dec 2016", "Apr 1927", "Oct 1502"], "%b %Y")).to.equal(true); | ||
expect(checkMatch(["December 03", "April 12", "October 21"], "%B %d")).to.equal(true); | ||
expect(checkMatch(["03 Dec", "03 Apr", "21 Oct"], "%d %b")).to.equal(true); | ||
expect(checkMatch(["2016", "1927", "1502"], "%Y")).to.equal(true); | ||
expect(checkMatch(["December", "April", "October"], "%B")).to.equal(true); | ||
expect(checkMatch(["Dec", "Apr", "Oct"], "%b")).to.equal(true); | ||
expect(checkMatch(["8:19:56 AM", "12:25:32 PM", "2:44:28 PM"], "%X")).to.equal(true); | ||
expect(checkMatch(["08:19 AM", "12:25 PM", "02:44 PM"], "%I:%M %p")).to.equal(true); | ||
expect(checkMatch(["8AM", "12PM", "2PM"], "%-I%p")).to.equal(true); | ||
expect(checkMatch(["08:19", "12:25", "14:44"], "%H:%M")).to.equal(true); | ||
}); | ||
it("should have option get/set methods with camelCase names", () => { | ||
const interpret = createInterpreter(); | ||
const checkMethod = function(prop, default_value, new_value) { | ||
var func = interpret[prop]; | ||
if (func() !== default_value) return false; | ||
if (func(new_value) !== interpret) return false; | ||
return func() === new_value; | ||
}; | ||
expect(checkMethod("nMax", 250, 100)).to.equal(true); | ||
expect(checkMethod("nFailingValues", 0, 1)).to.equal(true); | ||
expect(checkMethod("failureFraction", 5 / 100, 0.2)).to.equal(true); | ||
expect(checkMethod("sort", true, false)).to.equal(true); | ||
}); | ||
}); |
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
21212
365
1
1
62