Quick Take

import { strict as assert } from "assert";
import {
  lineCol,
  getLineStartIndexes,
} from "line-column-mini";

// index 14 is letter "k" on the fourth line:
assert.deepEqual(lineCol("abc\ndef\r\nghi\njkl", 14), {
  line: 4,
  col: 2,
});

// ---------------------------------------------------------

// if you know you might query multiple times, use caching
const lineIndexes = getLineStartIndexes(
  "abc\ndef\r\nghi\njkl"
);
assert.deepEqual(lineCol(lineIndexes, 14), {
  line: 4,
  col: 2,
});
// other queries will be by magnitude faster:
assert.deepEqual(lineCol(lineIndexes, 15), {
  line: 4,
  col: 3,
});

// by the way...
assert.deepEqual(lineCol(lineIndexes, 99), null);

Idea

The existing index-to line/column conversion program, popular and fast line-column opens in a new tab has many disadvantages:

  • Didn't support CR (old Mac opens in a new tab) line endings, only CRLF (Windows) and LF (modern Mac)
  • Didn't include type definitions natively...
  • ... and counterpart definitions on @types/line-column were wrong opens in a new tab (notice the missing LineColumnInfo export)
  • It was converting both ways, index-to line/column and line/column-to index, which, the source being not in ES modules, meant the unused functions would end up bundled too

No support for CR line endings was a blocker because we will use this program on emlint which is meant to catch such things.

This program, line-column-mini, is an alternative that only converts string index position to line/column number. It's as fast as line-column.

API - lineCol()

lineCol(
  input: string | number[],
  idx: number,
  skipChecks = false
)

In other words, it's a function which takes three input arguments, third-one optional:

Input argument Type Obligatory? Description
input string or number array yes Either a string or a result of getLineStartIndexes()
idx natural number or zero (a string index) yes A string index to convert
skipChecks boolean no Set it to true to skip all checks to increase perf even more

The function returns a null or a plain object like:

{
line: 4,
col: 3
}

Pre-calculating the line indexes using getLineStartIndexes() increases performance on subsequent calls — the program doesn't need to perform the first half of the calculations, finding each line start indexes.

API - getLineStartIndexes()

getLineStartIndexes(
  input: string
)

In other words, it's a function which takes three input arguments, third-one optional:

Input argument Type Obligatory? Description
input string yes The source string

The function returns an array of numbers, all string line start indexes, for example:

[0, 4, 6, 9, 12]

Feed this array to lineCol() instead of an input string.

Cutting corners

The algorithm works by first extracting an array of each line start indexes, then it performs a search on that array, searching where your given index does slot in.

If you calculate that indexes array once, you can reuse it in multiple calls (as long as the string is the same), improving the performance by a magnitude.

import { lineCol, getLineStartIndexes } from "line-column-mini";
import { strict as assert } from "assert";
const lineIndexes = getLineStartIndexes("abc\ndef\r\nghi\njkl");
// each call to lineCol() will cut corners:
assert.deepEqual(
lineCol(lineIndexes, 14),
{
line: 4,
col: 2,
}
);
assert.deepEqual(
lineCol(lineIndexes, 15),
{
line: 4,
col: 3,
}
);

// the full calculation would be equivalent but slower
assert.deepEqual(
lineCol("abc\ndef\r\nghi\njkl", 15),
{
line: 4,
col: 3,
}
);

Skipping checks

By the time you convert indexes to line/column position, you probably have done all the checks, you can guarantee that input string is not empty and so on. In those cases, you can force this program to skip the input validation checks by passing third argument as true:

import { lineCol, getLineStartIndexes } from "line-column-mini";
import { strict as assert } from "assert";
const input = "abc\ndef\r\nghi\rjkl";
assert.deepEqual(
lineCol(
input,
5,
true // <-------- skips all validation checks
),
{
line: 2,
col: 2,
}
);

That should make the program run a few percent faster.

Changelog

See it in the monorepo opens in a new tab, on GitHub.

Contributing

To report bugs or request features or assistance, raise an issue on GitHub opens in a new tab.

Any code contributions welcome! All Pull Requests will be dealt promptly.

Licence

MIT opens in a new tab

Copyright © 2010–2021 Roy Revelt and other contributors

Related packages:

📦 emlint 4.7.0
Pluggable email template code linter
📦 line-column opens in a new tab
Convert efficiently index to/from line-column in a string
📦 edit-package-json 0.4.0
Edit package.json without parsing, as string, to keep the formatting intact
📦 easy-replace 4.1.0
Replace strings with optional lookarounds, but without regexes
📦 str-indexes-of-plus 3.1.0
Like indexOf but returns array and counts per-grapheme
📦 email-all-chars-within-ascii 3.1.0
Scans all characters within a string and checks are they within ASCII range
📦 js-row-num 4.1.0
Update all row numbers in all console.logs in JS code