Browse Source

Core parser for 3.0 rewrite complete with all new tests

pull/62/head
Matthew Holt 11 years ago
parent
commit
42d707fe4a
  1. 306
      Parser.js
  2. 172
      test-runner.js
  3. 159
      tests.css
  4. 128
      tests.html
  5. 823
      tests.js

306
Parser.js

@ -0,0 +1,306 @@ @@ -0,0 +1,306 @@
/**
Papa Parse 3.0 alpha - core parser function
(c) 2014 Matthew Holt.
Not for use in production or redistribution.
For development of Papa Parse only.
**/
function Parser(config)
{
var self = this;
var BYTE_ORDER_MARK = "\ufeff";
var EMPTY = /^\s*$/;
// Delimiters that are not allowed
var _badDelimiters = ["\r", "\n", "\"", BYTE_ORDER_MARK];
var _input; // The input text being parsed
var _delimiter; // The delimiting character
var _comments; // Comment character (default '#') or boolean
var _step; // The step (streaming) function
var _callback; // The callback to invoke when finished
var _preview; // Maximum number of lines (not rows) to parse
var _ch; // Current character
var _i; // Current character's positional index
var _inQuotes; // Whether in quotes or not
var _lineNum; // Current line number (1-based indexing)
var _data; // Parsed data (results)
var _errors; // Parse errors
var _rowIdx; // Current row index within results (0-based)
var _colIdx; // Current col index within result row (0-based)
var _aborted; // Abort flag
var _paused; // Pause flag
// Unpack the config object
config = config || {};
_delimiter = config.delimiter;
_comments = config.comments;
_step = config.step;
_callback = config.complete;
_preview = config.preview;
// Delimiter integrity check
if (typeof _delimiter !== 'string'
|| _delimiter.length != 1
|| _badDelimiters.indexOf(_delimiter) > -1)
_delimiter = ",";
// Comment character integrity check
if (_comments === true)
_comments = "#";
else if (typeof _comments !== 'string'
|| _comments.length != 1
|| _badDelimiters.indexOf(_comments) > -1
|| _comments == _delimiter)
_comments = false;
// Parses delimited text input
this.parse = function(input)
{
if (typeof input !== 'string')
throw "Input must be a string";
reset(input);
return parserLoop();
};
this.pause = function()
{
_paused = true;
};
this.resume = function()
{
_paused = false;
if (_i < _input.length)
return parserLoop();
};
this.abort = function()
{
_aborted = true;
};
function parserLoop()
{
while (_i < _input.length)
{
if (_aborted) break;
if (_preview > 0 && _rowIdx >= _preview) break;
if (_paused) return;
if (_ch == '"')
parseQuotes();
else if (_inQuotes)
parseInQuotes();
else
parseNotInQuotes();
nextChar();
}
return finishParsing();
}
function nextChar()
{
_i++;
_ch = _input[_i];
}
function finishParsing()
{
if (_inQuotes)
addError("Quotes", "MissingQuotes", "Unescaped or mismatched quotes");
endRow(); // End of input is also end of the last row
if (typeof _step !== 'function')
return returnable();
else if (typeof _callback === 'function')
_callback();
}
function parseQuotes()
{
if (quotesOnBoundary() && !quotesEscaped())
_inQuotes = !_inQuotes;
else
{
saveChar();
if (_inQuotes && quotesEscaped())
_i++
else
addError("Quotes", "UnexpectedQuotes", "Unexpected quotes");
}
}
function parseInQuotes()
{
saveChar();
if (twoCharLineBreak())
{
nextChar();
saveChar();
_lineNum++;
}
else if (oneCharLineBreak())
_lineNum++;
}
function parseNotInQuotes()
{
if (_ch == _delimiter)
newField();
else if (twoCharLineBreak())
{
newRow();
nextChar();
}
else if (oneCharLineBreak())
newRow();
else if (isCommentStart())
skipLine();
else
saveChar();
}
function isCommentStart()
{
var firstCharOfLine = _i == 0
|| oneCharLineBreak(_i-1)
|| twoCharLineBreak(_i-2);
return firstCharOfLine && _input[_i] === _comments;
}
function skipLine()
{
while (!twoCharLineBreak()
&& !oneCharLineBreak()
&& _i < _input.length)
{
nextChar();
}
}
function saveChar()
{
_data[_rowIdx][_colIdx] += _ch;
}
function newField()
{
_data[_rowIdx].push("");
_colIdx = _data[_rowIdx].length - 1;
}
function newRow()
{
endRow();
_lineNum++;
_data.push([]);
_rowIdx = _data.length - 1;
newField();
}
function endRow()
{
trimEmptyLastRow();
if (typeof _step === 'function')
{
if (_data[_rowIdx])
_step(returnable(), self);
clearErrorsAndData();
}
}
function trimEmptyLastRow()
{
if (_data[_rowIdx].length == 1 && EMPTY.test(_data[_rowIdx][0]))
{
_data.splice(_rowIdx, 1);
_rowIdx = _data.length - 1;
}
}
function twoCharLineBreak(i)
{
if (typeof i !== 'number')
i = _i;
return i < _input.length - 1 &&
((_input[i] == "\r" && _input[i+1] == "\n")
|| (_input[i] == "\n" && _input[i+1] == "\r"))
}
function oneCharLineBreak(i)
{
if (typeof i !== 'number')
i = _i;
return _input[i] == "\r" || _input[i] == "\n";
}
function quotesEscaped()
{
// Quotes as data cannot be on boundary, for example: ,"", are not escaped quotes
return !quotesOnBoundary() && _i < _input.length - 1 && _input[_i+1] == '"';
}
function quotesOnBoundary()
{
return isBoundary(_i-1) || isBoundary(_i+1);
}
function isBoundary(i)
{
if (typeof i != 'number')
i = _i;
var ch = _input[i];
return (i == -1 || i == _input.length)
|| (i < _input.length
&& i > -1
&& (ch == _delimiter
|| ch == "\r"
|| ch == "\n"));
}
function addError(type, code, msg)
{
_errors.push({
type: type,
code: code,
message: msg,
line: _lineNum,
row: _rowIdx,
index: _i
});
}
function reset(input)
{
_input = input;
_inQuotes = false;
_lineNum = 1;
_i = 0;
clearErrorsAndData();
_data = [ [""] ]; // starting parsing requires an empty field
_ch = _input[_i];
}
function clearErrorsAndData()
{
_data = [];
_errors = [];
_rowIdx = 0;
_colIdx = 0;
}
function returnable()
{
return {
data: _data,
errors: _errors,
lines: _lineNum
};
}
}

172
test-runner.js

@ -0,0 +1,172 @@ @@ -0,0 +1,172 @@
var passCount = 0;
var failCount = 0;
$(function()
{
// First, wireup!
$('#results').on('click', 'td.rvl', function()
{
var tr = $(this).closest('tr');
if (tr.hasClass('collapsed'))
{
$('.revealer', tr).hide();
$('.hidden', tr).show();
$(this).html("-");
}
else
{
$('.revealer', tr).show();
$('.hidden', tr).hide();
$(this).html("+");
}
tr.toggleClass('collapsed expanded');
});
$('#expand-all').click(function()
{
$('.collapsed .rvl').click();
});
$('#collapse-all').click(function()
{
$('.expanded .rvl').click();
});
// Next, run tests and render results!
for (var i = 0; i < TESTS.length; i++)
{
var test = TESTS[i];
var passed = runTest(test, i);
if (passed)
passCount++;
else
failCount++;
}
// Finally, show the overall status.
if (failCount == 0)
$('#status').addClass('status-pass').html("All <b>"+passCount+"</b> test"+(passCount == 1 ? "" : "s")+" passed");
else
$('#status').addClass('status-fail').html("<b>"+failCount+"</b> test"+(failCount == 1 ? "" : "s")+" failed; <b>"+passCount+"</b> passed");
});
function runTest(test, num)
{
var parser = new Parser(test.config);
var actual = parser.parse(test.input);
var results = compare(actual.data, actual.errors, test.expected);
var testDescription = (test.description || "");
if (testDescription.length > 0)
testDescription += '<br>';
if (test.notes)
testDescription += '<span class="notes">' + test.notes + '</span>';
var tr = '<tr class="collapsed" id="test-'+num+'">'
+ '<td class="rvl">+</td>'
+ '<td>' + testDescription + '</td>'
+ passOrFailTd(results.data)
+ passOrFailTd(results.errors)
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">' + JSON.stringify(test.config, null, 2) + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">' + revealChars(test.input) + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">data: ' + JSON.stringify(test.expected.data, null, 4) + '\r\nerrors: ' + JSON.stringify(test.expected.errors, null, 4) + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">data: ' + JSON.stringify(actual.data, null, 4) + '\r\nerrors: ' + JSON.stringify(actual.errors, null, 4) + '</div></td>'
+ '</tr>';
$('#results').append(tr);
if (!results.data.passed || !results.errors.passed)
$('#test-'+num+' td.rvl').click();
return results.data.passed && results.errors.passed
}
function compare(actualData, actualErrors, expected)
{
var data = compareData(actualData, expected.data);
var errors = compareErrors(actualErrors, expected.errors);
return {
data: data,
errors: errors
}
}
function compareData(actual, expected)
{
var passed = true;
if (actual.length != expected.length)
passed = false;
for (var row = 0; row < expected.length; row++)
{
if (actual.length != expected.length)
{
passed = false;
break;
}
for (var col = 0; col < expected[row].length; col++)
{
if (actual[row].length != expected[row].length)
{
passed = false;
break;
}
var expectedVal = expected[row][col];
var actualVal = actual[row][col];
if (actualVal !== expectedVal)
{
passed = false;
break;
}
}
}
// We pass back an object right now, even though it only contains
// one value, because we might add details to the test results later
// (same with compareErrors below)
return {
passed: passed
};
}
function compareErrors(actual, expected)
{
var passed = JSON.stringify(actual) == JSON.stringify(expected);
return {
passed: passed
};
}
function passOrFailTd(result)
{
if (result.passed)
return '<td class="ok">OK</td>';
else
return '<td class="fail">FAIL</td>';
}
function revealChars(txt)
{
// Make spaces and tabs more obvious when glancing
txt = txt.replace(/( |\t)/ig, '<span class="whitespace-char">$1</span>');
txt = txt.replace(/(\r\n|\n\r|\r|\n)/ig, '<span class="whitespace-char special-char">$1</span>$1');
// Now make the line breaks within the spans actually appear on the page
txt = txt.replace(/">\r\n<\/span>/ig, '">\\r\\n</span>');
txt = txt.replace(/">\n\r<\/span>/ig, '">\\n\\r</span>');
txt = txt.replace(/">\r<\/span>/ig, '">\\r</span>');
txt = txt.replace(/">\n<\/span>/ig, '">\\n</span>');
return txt;
}

159
tests.css

@ -0,0 +1,159 @@ @@ -0,0 +1,159 @@
/* Eric Meyer's Reset CSS v2.0 */
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0}
body {
background: #F0F0F0;
font: 14px 'Helvetica Neue', sans-serif;
color: #333;
padding: 30px 15px;
}
a {
color: rgb(0, 142, 236);
}
a:hover {
color: rgb(82, 186, 255);
}
b {
font-weight: bold;
}
i {
font-style: italic;
}
h1 {
text-align: center;
font-weight: bold;
font-size: 62px;
margin-bottom: 30px;
}
.status-pass,
.status-fail {
padding: 10px;
margin-bottom: 30px;
color: #FFF;
text-align: center;
text-transform: uppercase;
font-size: 18px;
letter-spacing: 1px;
font-weight: 100;
}
.status-pass {
background: rgb(3, 168, 3);
}
.status-fail {
background: #BB0000;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 5px;
}
table th,
table td {
padding: 5px;
border: 1px solid #BBB;
}
table th {
color: #000;
background: #DDD;
font-weight: bold;
padding: 10px 5px;
text-transform: uppercase;
}
table td {
background: #FFF;
color: #555;
font-size: 14px;
}
td.ok,
td.fail {
text-transform: uppercase;
font-weight: 300;
vertical-align: middle;
text-align: center;
width: 80px;
}
td.ok {
background: rgb(204, 250, 144);
}
td.fail {
background: rgb(255, 192, 192);
}
td.rvl {
background: #444;
color: #999;
vertical-align: middle;
text-align: center;
cursor: pointer;
width: 20px;
}
td.rvl:hover {
color: #FFF;
}
tr.collapsed td.revealable {
background: #ECECEC;
vertical-align: middle;
text-align: center;
font-family: 'Helvetica Neue', sans-serif;
text-transform: lowercase;
color: #AAA;
}
tr.expanded .revealer {
font-family: 'Helvetica Neue', sans-serif;
text-transform: lowercase;
font-size: 10px;
background: #FFF;
position: absolute;
display: block;
padding: 3px;
top: -5px;
right: -5px;
}
td .container {
position: relative;
}
.notes {
color: #888;
font-size: 12px;
}
.pre {
font-family: Menlo, Monaco, monospace;
white-space: pre-wrap;
}
td.pre {
font-size: 12px;
}
.hidden {
display: none;
}
.special-char {
color: #78B7E7;
}
.whitespace-char {
background: #D5FCFA;
}

128
tests.html

@ -1,127 +1,33 @@ @@ -1,127 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>Parser Tests</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="jquery.parse.js"></script>
<title>Tests - Papa Parse</title>
<link rel="stylesheet" href="tests.css">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="Parser.js"></script>
<script src="tests.js"></script>
<style>
body {
font-family: sans-serif;
}
#tmp {
white-space: pre;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 10px;
}
#results {
border-collapse: collapse;
width: 100%;
}
#results td {
vertical-align: top;
padding: 10px;
border-bottom: 10px solid white;
}
#results td div {
overflow-x: auto;
}
.count {
background: #333;
color: #DDD;
width: 2em;
text-align: center;
}
.input,
.output {
width: 25%;
}
.input {
background: #DDD;
}
.config {
background: #CCC;
}
.output {
background: #EEE;
}
.input code,
.config code,
.output code {
font: 12px/1.5em 'Menlo', 'Monaco', 'Courier New', monospace;
display: block;
white-space: pre;
}
.clr-green,
.passing {
color: #475B15;
}
.clr-red,
.failing {
color: #AA0000;
}
.passing {
background: #ECF9CC;
color: #475B15;
}
.failing {
background: #FFE8E8;
}
.failing code {
font-weight: bold;
}
hr {
border: 0;
background: 0;
clear: both;
}
<script src="test-runner.js"></script>
</head>
<body>
<h1>Papa Parse Tests</h1>
.clr-green {
color: #79A01E;
}
<div id="status"></div>
.clr-red {
color: #AA0000;
}
<a href="javascript:" id="expand-all">Expand all</a>
&middot;
<a href="javascript:" id="collapse-all">Collapse all</a>
<br>
#pass-count,
#fail-count {
font-weight: bold;
}
</style>
</head>
<body>
SUMMARY
&nbsp;
<span class="clr-green"><span id="pass-count">-</span> PASS</span>
&nbsp;
<span class="clr-red"><span id="fail-count">-</span> FAIL</span>
<br><br>
<table id="results">
<tr>
<th></th>
<th>Input</th>
<th colspan="2">Test Case</th>
<th>Data</th>
<th>Errors</th>
<th>Config</th>
<th>Input</th>
<th>Expected</th>
<th>Actual</th>
</tr>
</table>
<div id="output"></div>
</body>
</html>

823
tests.js

@ -1,693 +1,384 @@ @@ -1,693 +1,384 @@
var passCount = 0, failCount = 0;
var passing = "passing";
var failing = "failing";
var RECORD_SEP = String.fromCharCode(30);
var UNIT_SEP = String.fromCharCode(31);
var recordSep = String.fromCharCode(30);
var unitSep = String.fromCharCode(31);
var resultSet1 = [
var TESTS = [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
input: 'A,b,c',
description: "One row",
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": 2,
"F3": "V3"
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
data: [['A', 'b', 'c']],
errors: []
}
},
{
config: { delimiter: ",", header: false, dynamicTyping: true },
input: 'A,b,c\r\nd,E,f',
description: "Two rows",
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
2,
"V3"
]
],
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
data: [['A', 'b', 'c'], ['d', 'E', 'f']],
errors: []
}
},
{
config: { delimiter: ",", header: false, dynamicTyping: false },
input: 'A,b,c\rd,E,f',
description: "Two rows, just \\r",
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
"2",
"V3"
]
],
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
data: [['A', 'b', 'c'], ['d', 'E', 'f']],
errors: []
}
},
{
config: { delimiter: ",", header: true, dynamicTyping: false },
input: 'A,b,c\nd,E,f',
description: "Two rows, just \\n",
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": "2",
"F3": "V3"
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
data: [['A', 'b', 'c'], ['d', 'E', 'f']],
errors: []
}
},
{
config: { delimiter: "", header: true, dynamicTyping: true },
input: 'a, b ,c',
description: "Whitespace at edges of unquoted field",
notes: "Extra whitespace should graciously be preserved",
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": 2,
"F3": "V3"
data: [['a', ' b ', 'c']],
errors: []
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
},
];
var tests = [
{
input: "F1,F2,F3\nV1,2,V3",
cases: resultSet1
},
{
input: "F1,F2,F3\r\nV1,2,V3",
cases: resultSet1
},
{
input: "F1,\"F2\",F3\r\nV1,2,\"V3\"",
cases: resultSet1
},
{
input: "F1,F2,F3\n\nV1,2,V3",
cases: resultSet1
input: 'A,"B",C',
description: "Quoted field",
expected: {
data: [['A', 'B', 'C']],
errors: []
}
},
{
input: "F1,F2,F3\r\n\r\nV1,2,V3",
cases: resultSet1
input: 'A," B ",C',
description: "Quoted field with extra whitespace on edges",
expected: {
data: [['A', ' B ', 'C']],
errors: []
}
},
{
input: "F1,F2,F3\n\rV1,2,V3",
cases: resultSet1
input: 'A,"B,B",C',
description: "Quoted field with delimiter",
expected: {
data: [['A', 'B,B', 'C']],
errors: []
}
},
{
input: "F1,F2,F3\rV1,2,V3",
cases: resultSet1
input: 'A,"B\r\nB",C',
description: "Quoted field with \\r\\n",
expected: {
data: [['A', 'B\r\nB', 'C']],
errors: []
}
},
{
input: "F1,F2,F3\r\n \r\nV1,2,V3",
cases: resultSet1
input: 'A,"B\rB",C',
description: "Quoted field with \\r",
expected: {
data: [['A', 'B\rB', 'C']],
errors: []
}
},
{
input: "\r\nF1,F2,F3\r\nV1,2,V3",
cases: resultSet1
input: 'A,"B\nB",C',
description: "Quoted field with \\n",
expected: {
data: [['A', 'B\nB', 'C']],
errors: []
}
},
{
input: 'F1,F2,"F3"\n"V1","2",V3',
cases: resultSet1
input: 'A,"B""B""B",C',
description: "Quoted field with escaped quotes",
expected: {
data: [['A', 'B"B"B', 'C']],
errors: []
}
},
{
input: "F1,F2,F3\nV1,2,V3\nV4,V5,V6",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
input: 'A,"""B""",C',
description: "Quoted field with escaped quotes at boundaries",
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": 2,
"F3": "V3"
data: [['A', '"B"', 'C']],
errors: []
}
},
{
"F1": "V4",
"F2": "V5",
"F3": "V6"
}
input: 'A, "B" ,C',
description: "Quoted field with whitespace around quotes",
notes: "This is malformed input, but it should be parsed gracefully (with errors)",
expected: {
data: [['A', ' "B" ', 'C']],
errors: [
{"type": "Quotes", "code": "UnexpectedQuotes", "message": "Unexpected quotes", "line": 1, "row": 0, "index": 3},
{"type": "Quotes", "code": "UnexpectedQuotes", "message": "Unexpected quotes", "line": 1, "row": 0, "index": 5}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
},
{
config: { delimiter: ",", header: false, dynamicTyping: true },
input: 'a\tb\tc\r\nd\te\tf',
config: { delimiter: "\t" },
description: "Tab delimiter",
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
2,
"V3"
],
[
"V4",
"V5",
"V6"
]
],
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
config: { delimiter: ",", header: false, dynamicTyping: false },
input: 'a|b|c\r\nd|e|f',
config: { delimiter: "|" },
description: "Pipe delimiter",
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
"2",
"V3"
],
[
"V4",
"V5",
"V6"
]
],
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
config: { delimiter: ",", header: true, dynamicTyping: false },
input: 'a'+RECORD_SEP+'b'+RECORD_SEP+'c\r\nd'+RECORD_SEP+'e'+RECORD_SEP+'f',
config: { delimiter: RECORD_SEP },
description: "ASCII 30 delimiter",
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": "2",
"F3": "V3"
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
"F1": "V4",
"F2": "V5",
"F3": "V6"
input: 'a'+UNIT_SEP+'b'+UNIT_SEP+'c\r\nd'+UNIT_SEP+'e'+UNIT_SEP+'f',
config: { delimiter: UNIT_SEP },
description: "ASCII 31 delimiter",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
{
input: 'aDELIMbDELIMc',
config: { delimiter: "DELIM" },
description: "Bad delimiter",
notes: "Should silently default to comma",
expected: {
data: [['aDELIMbDELIMc']],
errors: []
}
]
},
{
input: "F1,F2,F3\n,2,V3\nV4,V5,V6",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
input: '# Comment!\r\na,b,c',
config: { comments: true },
description: "Commented line at beginning (comments: true)",
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "",
"F2": 2,
"F3": "V3"
data: [['a', 'b', 'c']],
errors: []
}
},
{
"F1": "V4",
"F2": "V5",
"F3": "V6"
input: 'a,b,c\r\n# Comment\r\nd,e,f',
config: { comments: true },
description: "Commented line in middle (comments: true)",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
{
input: 'a,b,c\r\n# Comment',
config: { comments: true },
description: "Commented line at end (comments: true)",
expected: {
data: [['a', 'b', 'c']],
errors: []
}
]
},
{
input: "F1,F2,F3\n,2,V3,V4\nV5,V6,V7",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
input: 'a,b,c\r\n!Comment goes here\r\nd,e,f',
config: { comments: '!' },
description: "Comment with non-default character (comments: '!')",
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "",
"F2": 2,
"F3": "V3",
"__parsed_extra": [
"V4"
]
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
"F1": "V5",
"F2": "V6",
"F3": "V7"
input: 'a,b,c\r\n=N(Comment)\r\nd,e,f',
config: { comments: '=N(' },
description: "Comment, but bad char specified (comments: \"=N(\")",
notes: "Should silently disable comment parsing",
expected: {
data: [['a', 'b', 'c'], ['=N(Comment)'], ['d', 'e', 'f']],
errors: []
}
]
},
"errors": {
"0": [
{
"type": "FieldMismatch",
"code": "TooManyFields",
"message": "Too many fields: expected 3 fields but parsed 4",
"line": 2,
"row": 0,
"index": 17
input: '#commented line\r\n',
config: { comments: true },
description: "Input with only a commented line (comments: true)",
expected: {
data: [],
errors: []
}
],
"length": 1
},
"meta": {
"delimiter": ","
}
}
{
input: '#commented line',
description: "Input with comment without comments enabled",
expected: {
data: [['#commented line']],
errors: []
}
]
},
{
input: "F1,F2,F3\nV1,2.0,-3.01, V4\n\rV5,\"V\n6\",V7\r,,",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
input: 'a\r\n b\r\nc',
description: "Input without comments with line starting with whitespace",
notes: "\" \" == false, but \" \" !== false, so === comparison is required",
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": 2,
"F3": -3.01,
"__parsed_extra": [
" V4"
]
data: [['a'], [' b'], ['c']],
errors: []
}
},
{
"F1": "V5",
"F2": "V\n6",
"F3": "V7"
input: 'a#b#c\r\n# Comment',
config: { delimiter: '#', comments: '#' },
description: "Comment char same as delimiter",
notes: "Comment parsing should automatically be silently disabled in this case",
expected: {
data: [['a', 'b', 'c'], ['', ' Comment']],
errors: []
}
},
{
"F1": "",
"F2": "",
"F3": ""
input: '\r\na,b,c\r\nd,e,f',
description: "Blank line at beginning",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
]
},
"errors": {
"0": [
{
"type": "FieldMismatch",
"code": "TooManyFields",
"message": "Too many fields: expected 3 fields but parsed 4",
"line": 2,
"row": 0,
"index": 25
input: 'a,b,c\r\n\r\nd,e,f',
description: "Blank line in middle",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
],
"length": 1
},
"meta": {
"delimiter": ","
}
}
{
input: 'a,b,c\r\nd,e,f\r\n\r\n',
description: "Blank lines at end",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
]
},
{
input: "F1,F2,F3\nV1,V2,V3\nV5,\"V6,V7",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
input: 'a,b,c\r\n \r\nd,e,f',
description: "Blank line in middle with whitespace",
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": "V1",
"F2": "V2",
"F3": "V3"
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{
"F1": "V5",
"F2": "V6,V7"
input: 'a,b,c\r\n,e,f',
description: "First field of a line is empty",
expected: {
data: [['a', 'b', 'c'], ['', 'e', 'f']],
errors: []
}
]
},
"errors": {
"1": [
{
"type": "FieldMismatch",
"code": "TooFewFields",
"message": "Too few fields: expected 3 fields but parsed 2",
"line": 3,
"row": 1,
"index": 27
input: 'a,b,c\r\n,e,f',
description: "First field of a line is empty",
expected: {
data: [['a', 'b', 'c'], ['', 'e', 'f']],
errors: []
}
},
{
"type": "Quotes",
"code": "MissingQuotes",
"message": "Unescaped or mismatched quotes",
"line": 3,
"row": 1,
"index": 27
input: 'a,,c\r\n,,',
description: "Other fields are empty",
expected: {
data: [['a', '', 'c'], ['', '', '']],
errors: []
}
],
"length": 2
},
"meta": {
"delimiter": ","
}
}
{
input: '',
description: "Empty input string",
expected: {
data: [],
errors: []
}
]
},
{
input: "F1,F2,F3\n2,-2, 2\n 2. ,.2, .2 \n-2., -2.0e-5, -.4 ",
cases: [
{
config: { delimiter: ",", header: true, dynamicTyping: true },
input: ',',
description: "Input is just the delimiter (2 empty fields)",
expected: {
"results": {
"fields": [
"F1",
"F2",
"F3"
],
"rows": [
{
"F1": 2,
"F2": -2,
"F3": 2
data: [['', '']],
errors: []
}
},
{
"F1": 2,
"F2": 0.2,
"F3": 0.2
input: 'Abc def',
description: "Input is just a string (a single field)",
expected: {
data: [['Abc def']],
errors: []
}
},
{
"F1": -2,
"F2": -0.00002,
"F3": -0.4
input: 'a,b,c\r\nd,e,f\r\ng,h,i',
config: { preview: 0 },
description: "Preview 0 rows should default to parsing all",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']],
errors: []
}
]
},
"errors": {
"length": 0
},
"meta": {
"delimiter": ","
}
}
{
input: 'a,b,c\r\nd,e,f\r\ng,h,i',
config: { preview: 1 },
description: "Preview 1 row",
expected: {
data: [['a', 'b', 'c']],
errors: []
}
]
},
{
input: "F1\nV1\nV2\nV3\nV4",
cases: [
{
config: { delimiter: "", header: false, dynamicTyping: false },
expected: {
"results": [
[
"F1"
],
[
"V1"
],
[
"V2"
],
[
"V3"
],
[
"V4"
]
],
"errors": {
"length": 1,
"config": [
{
"type": "Delimiter",
"code": "UndetectableDelimiter",
"message": "Unable to auto-detect delimiting character; defaulted to comma",
"line": 1,
"row": 0,
"index": 0
input: 'a,b,c\r\nd,e,f\r\ng,h,i',
config: { preview: 2 },
description: "Preview 2 rows",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
]
},
"meta": {
"delimiter": ","
}
}
{
input: 'a,b,c\r\nd,e,f\r\ng,h,i',
config: { preview: 3 },
description: "Preview all (3) rows",
expected: {
data: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']],
errors: []
}
]
},
{
input: ["F1","F2","F3\r\nV1","V2","V3"].join(recordSep),
cases: [
{
config: { delimiter: "", header: false, dynamicTyping: false },
input: 'a,b,c\r\nd,e,f\r\ng,h,i',
config: { preview: 4 },
description: "Preview more rows than input has",
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
"V2",
"V3"
],
],
"errors": {
"length": 0
},
"meta": {
"delimiter": "\u001e"
data: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']],
errors: []
}
}
}
]
},
{
input: ["F1","F2","F3\r\nV1","V2","V3"].join(unitSep),
cases: [
{
config: { delimiter: "", header: false, dynamicTyping: false },
input: 'a,b,c\r\nd,e,"f\r\nf",g,h,i',
config: { preview: 2 },
description: "Preview should count rows, not lines",
expected: {
"results": [
[
"F1",
"F2",
"F3"
],
[
"V1",
"V2",
"V3"
],
],
"errors": {
"length": 0
},
"meta": {
"delimiter": "\u001f"
data: [['a', 'b', 'c'], ['d', 'e', 'f\r\nf', 'g', 'h', 'i']],
errors: []
}
}
}
]
}
];
$(function()
{
var counter = 0;
for (var i = 0; i < tests.length; i++)
{
var test = tests[i];
var input = test.input;
for (var j = 0; j < test.cases.length; j++)
{
counter++;
var testCase = test.cases[j];
var actual = doTest(input, testCase.config);
var status = equal(actual, testCase.expected) ? passing : failing;
render(input, testCase.expected, actual, testCase.config, counter, status);
}
}
$('#pass-count').text(passCount);
$('#fail-count').text(failCount);
});
function doTest(input, config)
{
// try
// {
return $.parse(input, config);
// }
// catch (e)
// {
// return {exception: e.message, error: e, note: "See console to inspect stack"};
// }
}
function render(input, expected, actual, config, count, status)
{
if (status == passing)
passCount++;
else
{
console.log("TEST " + count +" FAILED.");
console.log(" Expected:", expected);
console.log(" Actual:", actual);
console.log(" Config:", config);
failCount++;
}
var html = '<tr>' +
'<td class="count">'+count+'</td>' +
'<td class="input"><div><code>'+string(input)+'</code></div></td>' +
'<td class="config"><div><code>'+string(config)+'</code></div></td>' +
'<td class="output"><div><code>'+string(expected)+'</code></div></td>' +
'<td class="output '+status+'"><div><code>'+string(actual)+'</code></div></td>' +
'</tr>';
$('#results').append(html);
}
function string(obj)
{
return typeof obj === "string" ? obj : JSON.stringify(obj, undefined, 2);
}
function equal(actual, expected)
{
return string(actual) === string(expected);
}
Loading…
Cancel
Save