add alignTable, parseTableCells to align/format a tab(etc) delimited table (#10182)

* add compiler/unittest_light.nim for easy diffing: assertEquals and mismatch
* fixup
* add alignTable, parseTableCells
This commit is contained in:
Timothee Cour
2019-01-09 00:46:44 -08:00
committed by Andreas Rumpf
parent 258952832f
commit 23c1ee982e
5 changed files with 291 additions and 0 deletions

83
compiler/asciitables.nim Normal file
View File

@@ -0,0 +1,83 @@
#[
move to std/asciitables.nim once stable, or to a nimble paackage
once compiler can depend on nimble
]#
type Cell* = object
text*: string
width*, row*, col*, ncols*, nrows*: int
iterator parseTableCells*(s: string, delim = '\t'): Cell =
## iterates over all cells in a `delim`-delimited `s`, after a 1st
## pass that computes number of rows, columns, and width of each column.
var widths: seq[int]
var cell: Cell
template update() =
if widths.len<=cell.col:
widths.setLen cell.col+1
widths[cell.col] = cell.width
else:
widths[cell.col] = max(widths[cell.col], cell.width)
cell.width = 0
for a in s:
case a
of '\n':
update()
cell.col = 0
cell.row.inc
elif a == delim:
update()
cell.col.inc
else:
# todo: consider multi-width chars when porting to non-ascii implementation
cell.width.inc
if s.len > 0 and s[^1] != '\n':
update()
cell.ncols = widths.len
cell.nrows = cell.row + 1
cell.row = 0
cell.col = 0
cell.width = 0
template update2() =
cell.width = widths[cell.col]
yield cell
cell.text = ""
cell.width = 0
cell.col.inc
template finishRow() =
for col in cell.col..<cell.ncols:
cell.col = col
update2()
cell.col = 0
for a in s:
case a
of '\n':
finishRow()
cell.row.inc
elif a == delim:
update2()
else:
cell.width.inc
cell.text.add a
if s.len > 0 and s[^1] != '\n':
finishRow()
proc alignTable*(s: string, delim = '\t', fill = ' ', sep = " "): string =
## formats a `delim`-delimited `s` representing a table; each cell is aligned
## to a width that's computed for each column; consecutive columns are
## delimted by `sep`, and alignment space is filled using `fill`.
## More customized formatting can be done by calling `parseTableCells` directly.
for cell in parseTableCells(s, delim):
result.add cell.text
for i in cell.text.len..<cell.width:
result.add fill
if cell.col < cell.ncols-1:
result.add sep
if cell.col == cell.ncols-1 and cell.row < cell.nrows - 1:
result.add '\n'

View File

@@ -0,0 +1,37 @@
# note: consider merging tests/assert/testhelper.nim here.
proc mismatch*[T](lhs: T, rhs: T): string =
## Simplified version of `unittest.require` that satisfies a common use case,
## while avoiding pulling too many dependencies. On failure, diagnostic
## information is provided that in particular makes it easy to spot
## whitespace mismatches and where the mismatch is.
proc replaceInvisible(s: string): string =
for a in s:
case a
of '\n': result.add "\\n\n"
else: result.add a
proc quoted(s: string): string = result.addQuoted s
result.add "\n"
result.add "lhs:{\n" & replaceInvisible(
$lhs) & "}\nrhs:{\n" & replaceInvisible($rhs) & "}\n"
when compiles(lhs.len):
if lhs.len != rhs.len:
result.add "lhs.len: " & $lhs.len & " rhs.len: " & $rhs.len & "\n"
when compiles(lhs[0]):
var i = 0
while i < lhs.len and i < rhs.len:
if lhs[i] != rhs[i]: break
i.inc
result.add "first mismatch index: " & $i & "\n"
if i < lhs.len and i < rhs.len:
result.add "lhs[i]: {" & quoted($lhs[i]) & "} rhs[i]: {" & quoted(
$rhs[i]) & "}"
result.add "lhs[0..<i]:{\n" & replaceInvisible($lhs[
0..<i]) & "}\nrhs[0..<i]:{\n" & replaceInvisible($rhs[0..<i]) & "}"
proc assertEquals*[T](lhs: T, rhs: T) =
when false: # can be useful for debugging to see all that's fed to this.
echo "----" & $lhs
doAssert lhs==rhs, mismatch(lhs, rhs)

7
tests/compiler/nim.cfg Normal file
View File

@@ -0,0 +1,7 @@
# note: consider moving tests/compilerapi/ to tests/compiler/ since
# that's more predictable.
# note: without this, tests may succeed locally but fail on CI (it can succeed
# locally even when compiling via `./bin/nim` because `$HOME/.nimble` is being
# used).
--path:"../../" # so we can `import compiler/foo` in this dir

View File

@@ -0,0 +1,109 @@
import compiler/unittest_light
import compiler/asciitables
import strformat
proc alignTableCustom(s: string, delim = '\t', sep = ","): string =
for cell in parseTableCells(s, delim):
result.add fmt"({cell.row},{cell.col}): "
for i in cell.text.len..<cell.width:
result.add " "
result.add cell.text
if cell.col < cell.ncols-1:
result.add sep
if cell.col == cell.ncols-1 and cell.row < cell.nrows - 1:
result.add '\n'
proc testAlignTable() =
block: # test with variable width columns
var ret = ""
ret.add "12\t143\tbcdef\n"
ret.add "2\t14394852020\tbcdef\n"
ret.add "45342\t1\tbf\n"
ret.add "45342\t1\tbsadfasdfasfdasdff\n"
ret.add "453232323232342\t1\tbsadfasdfasfdasdff\n"
ret.add "45342\t1\tbf\n"
ret.add "45342\t1\tb afasf a ff\n"
ret.add "4\t1\tbf\n"
assertEquals alignTable(ret),
"""
12 143 bcdef
2 14394852020 bcdef
45342 1 bf
45342 1 bsadfasdfasfdasdff
453232323232342 1 bsadfasdfasfdasdff
45342 1 bf
45342 1 b afasf a ff
4 1 bf
"""
assertEquals alignTable(ret, fill = '.', sep = ","),
"""
12.............,143........,bcdef.............
2..............,14394852020,bcdef.............
45342..........,1..........,bf................
45342..........,1..........,bsadfasdfasfdasdff
453232323232342,1..........,bsadfasdfasfdasdff
45342..........,1..........,bf................
45342..........,1..........,b afasf a ff......
4..............,1..........,bf................
"""
assertEquals alignTableCustom(ret, sep = " "),
"""
(0,0): 12 (0,1): 143 (0,2): bcdef
(1,0): 2 (1,1): 14394852020 (1,2): bcdef
(2,0): 45342 (2,1): 1 (2,2): bf
(3,0): 45342 (3,1): 1 (3,2): bsadfasdfasfdasdff
(4,0): 453232323232342 (4,1): 1 (4,2): bsadfasdfasfdasdff
(5,0): 45342 (5,1): 1 (5,2): bf
(6,0): 45342 (6,1): 1 (6,2): b afasf a ff
(7,0): 4 (7,1): 1 (7,2): bf
"""
block: # test with 1 column
var ret = "12\nasdfa\nadf"
assertEquals alignTable(ret), """
12
asdfa
adf """
block: # test with empty input
var ret = ""
assertEquals alignTable(ret), ""
block: # test with 1 row
var ret = "abc\tdef"
assertEquals alignTable(ret), """
abc def"""
block: # test with 1 row ending in \t
var ret = "abc\tdef\t"
assertEquals alignTable(ret), """
abc def """
block: # test with 1 row starting with \t
var ret = "\tabc\tdef\t"
assertEquals alignTable(ret), """
abc def """
block: # test with variable number of cols per row
var ret = """
a1,a2,a3
b1
c1,c2
,d1
"""
assertEquals alignTableCustom(ret, delim = ',', sep = ","),
"""
(0,0): a1,(0,1): a2,(0,2): a3
(1,0): ,(1,1): ,(1,2):
(2,0): b1,(2,1): ,(2,2):
(3,0): c1,(3,1): c2,(3,2):
(4,0): ,(4,1): d1,(4,2):
"""
testAlignTable()

View File

@@ -0,0 +1,55 @@
import compiler/unittest_light
proc testAssertEquals() =
assertEquals("foo", "foo")
doAssertRaises(AssertionError):
assertEquals("foo", "foo ")
proc testMismatch() =
assertEquals(1+1, 2*1)
let a = """
some test with space at the end of lines
can be hard to spot differences when diffing in a terminal
without this helper function
"""
let b = """
some test with space at the end of lines
can be hard to spot differences when diffing in a terminal
without this helper function
"""
doAssert mismatch(a, b) == """
lhs:{
some test with space at the end of lines \n
\n
can be hard to spot differences when diffing in a terminal \n
without this helper function\n
\n
}
rhs:{
some test with space at the end of lines \n
\n
can be hard to spot differences when diffing in a terminal \n
without this helper function\n
\n
}
lhs.len: 144 rhs.len: 143
first mismatch index: 110
lhs[i]: {" "} rhs[i]: {"\n"}lhs[0..<i]:{
some test with space at the end of lines \n
\n
can be hard to spot differences when diffing in a terminal }
rhs[0..<i]:{
some test with space at the end of lines \n
\n
can be hard to spot differences when diffing in a terminal }"""
testMismatch()
testAssertEquals()