Skip to content

Commit eb1d7b3

Browse files
authored
Preserve line endings (#49)
* bump versions * failing tests * fix the bug * one more test * version bump * prettier * format on save
1 parent 1b1da5f commit eb1d7b3

File tree

7 files changed

+75
-11
lines changed

7 files changed

+75
-11
lines changed

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
{
2-
"typescript.tsdk": "node_modules/typescript/lib"
2+
"typescript.tsdk": "node_modules/typescript/lib",
3+
"[typescript]": {
4+
"editor.formatOnSave": true
5+
}
36
}

classify-images.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ interface CLIArgs {
3838

3939
const program = new Command();
4040
program
41-
.version('2.2.0')
41+
.version('2.2.1')
4242
.usage('[options] /path/to/images/*.jpg | images.txt')
4343
.option('-p, --port <n>', 'Run on this port (default 4321)', parseInt)
4444
.option('-o, --output <file>', 'Path to output CSV file (default output.csv)', 'output.csv')

csv.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import csvParse from 'csv-parse';
2-
import {stringify} from 'csv-stringify/sync';
2+
import {stringify, Options} from 'csv-stringify/sync';
33
import * as fs from 'fs-extra';
44

55
const csvOptions: csvParse.Options = {
@@ -83,12 +83,35 @@ export async function readHeaders(file: string) {
8383
}
8484

8585
/** Write a CSV file */
86-
export async function writeCsv(file: string, rows: string[][]) {
86+
export async function writeCsv(file: string, rows: string[][], options?: Options) {
8787
// TODO(danvk): make this less memory-intensive
88-
const output = stringify(rows);
88+
const output = stringify(rows, options);
8989
await fs.writeFile(file, output, {encoding: 'utf8'});
9090
}
9191

92+
const LF = '\n'.charCodeAt(0);
93+
const CR = '\r'.charCodeAt(0);
94+
95+
/** Determine the type of line endings a file uses by looking for the first one. */
96+
export function detectLineEnding(path: string) {
97+
const f = fs.openSync(path, 'r');
98+
const SIZE = 10_000;
99+
const buffer = Buffer.alloc(SIZE);
100+
const n = fs.readSync(f, buffer, 0, SIZE, 0);
101+
fs.closeSync(f);
102+
for (let i = 0; i < n - 1; i++) {
103+
const [a, b] = [buffer[i], buffer[i + 1]];
104+
if (a == CR && b == LF) {
105+
return '\r\n'; // Windows
106+
} else if (a == LF) {
107+
return '\n'; // Unix
108+
} else if (a == CR) {
109+
return '\r'; // Old Mac
110+
}
111+
}
112+
return undefined;
113+
}
114+
92115
/**
93116
* Append one row to a CSV file.
94117
*
@@ -103,6 +126,7 @@ export async function appendRow(file: string, row: {[column: string]: string}) {
103126
return writeCsv(file, rows);
104127
}
105128

129+
const lineEnding = detectLineEnding(file);
106130
const lines = readRows(file);
107131
const headerRow = await lines.next();
108132
if (headerRow.done) {
@@ -130,22 +154,25 @@ export async function appendRow(file: string, row: {[column: string]: string}) {
130154
rows.push(row.concat(emptyCols));
131155
}
132156
rows.push(fullHeaders.map(k => row[k] || ''));
133-
await writeCsv(file, rows);
157+
await writeCsv(file, rows, {record_delimiter: lineEnding});
134158
} else {
135159
// write the new row
136160
const newRow = headers.map(k => row[k] || '');
137161
await lines.return(); // close the file for reading.
138162
// Add a newline if the file doesn't end with one.
139163
const f = fs.openSync(file, 'a+');
140164
const {size} = fs.fstatSync(f);
141-
const {buffer} = await fs.read(f, Buffer.alloc(1), 0, 1, size - 1);
142-
const hasTrailingNewline = buffer[0] == '\n'.charCodeAt(0);
143-
const lineStr = (hasTrailingNewline ? '' : '\n') + stringify([newRow]);
165+
const {buffer} = await fs.read(f, Buffer.alloc(2), 0, 2, size - 2);
166+
const tail = buffer.toString('utf8');
167+
const hasTrailingNewline = tail.endsWith(lineEnding ?? '\n');
168+
const lineStr =
169+
(hasTrailingNewline ? '' : lineEnding) + stringify([newRow], {record_delimiter: lineEnding});
144170
await fs.appendFile(f, lineStr);
145171
await fs.close(f);
146172
}
147173
}
148174

175+
// Note: this might change line endings in the file.
149176
export async function deleteLastRow(file: string) {
150177
const rows = [];
151178
for await (const row of readRows(file)) {

localturk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const program = new Command();
4545

4646
// If you add an option here, consider adding it in classify-images.ts as well.
4747
program
48-
.version('2.2.0')
48+
.version('2.2.1')
4949
.usage('[options] template.html tasks.csv outputs.csv')
5050
.option('-p, --port <n>', 'Run on this port (default 4321)', parseInt)
5151
.option(

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "localturk",
3-
"version": "2.2.0",
3+
"version": "2.2.1",
44
"description": "Run Mechanical Turk-like tasks on your own.",
55
"main": "index.js",
66
"repository": "https://github.com/danvk/localturk.git",

test/csv.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,36 @@ describe('csv', () => {
143143
const data = await read('/tmp/test.csv');
144144
expect(data).toEqual('id,"First,Last","Last,First"\n' + '1,"Jane,Doe","Doe,Jane"\n');
145145
});
146+
147+
it('should read rows from a CSV file with Windows line endings', async () => {
148+
const rows = [];
149+
for await (const row of csv.readRows('./test/windows.csv')) {
150+
rows.push(row);
151+
}
152+
expect(rows).toEqual([
153+
['A', 'B'],
154+
['1', '2'],
155+
]);
156+
});
157+
158+
it('should append to a CSV file with Windows line endings', async () => {
159+
fs.copyFileSync('./test/windows.csv', '/tmp/test.csv');
160+
expect(csv.detectLineEnding('/tmp/test.csv')).toEqual('\r\n');
161+
await csv.appendRow('/tmp/test.csv', {A: '3', B: '4'});
162+
expect(await read('/tmp/test.csv')).toEqual(`A,B\r\n1,2\r\n3,4\r\n`);
163+
});
164+
165+
it('should append to a CSV file with Windows line endings and no trailing newline', async () => {
166+
fs.writeFileSync('/tmp/test.csv', `A,B\r\n1,2`);
167+
expect(csv.detectLineEnding('/tmp/test.csv')).toEqual('\r\n');
168+
expect(await read('/tmp/test.csv')).toEqual(`A,B\r\n1,2`); // no trailing newline
169+
await csv.appendRow('/tmp/test.csv', {A: '3', B: '4'});
170+
expect(await read('/tmp/test.csv')).toEqual(`A,B\r\n1,2\r\n3,4\r\n`);
171+
});
172+
173+
it('should preserve line endings when adding a new column', async () => {
174+
fs.copyFileSync('./test/windows.csv', '/tmp/test.csv');
175+
await csv.appendRow('/tmp/test.csv', {A: '3', C: '4'});
176+
expect(await read('/tmp/test.csv')).toEqual(`A,B,C\r\n1,2,\r\n3,,4\r\n`);
177+
});
146178
});

test/windows.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
A,B
2+
1,2

0 commit comments

Comments
 (0)