-
Notifications
You must be signed in to change notification settings - Fork 7
/
align.go
366 lines (313 loc) · 9.43 KB
/
align.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
package align
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
"github.com/mattn/go-runewidth"
)
// Justification is used to set the alignment of the column
// contents itself along the right, left, or center.
type Justification byte
// Left, Right or Center Justification options.
const (
JustifyRight Justification = iota + 1
JustifyCenter
JustifyLeft
)
// TextQualifier is used to configure the scanner to account for a text qualifier.
type TextQualifier struct {
On bool
Qualifier string
}
// PaddingOpts provides configurability for left/center/right Justification and padding length.
type PaddingOpts struct {
Justification Justification
ColumnOverride map[int]Justification //override the Justification of specified columns
Pad int // padding surrounding the separator
}
// Grower grows by the given number of bytes n.
// Reset will set the Grower to 0.
type Grower interface {
Grow(n int)
Reset()
}
// Padder builds a string and can return its string value.
type Padder interface {
io.Writer
fmt.Stringer
WriteByte(c byte) error
WriteString(s string) (int, error)
Bytes() []byte
}
// PadGrower makes a string with the ability to
// grow the underlying buffer.
type PadGrower interface {
Grower
Padder
}
// fieldPad ructs the contents of a field
// with padding.
type fieldPad struct {
bytes.Buffer
}
// Align scans input and writes output with aligned text.
type Align struct {
scanner *bufio.Scanner
writer *bufio.Writer
sep string // separator string or delimiter
sepOut string
columnCounts map[int]int
txtq TextQualifier
padOpts PaddingOpts
filter []int
filterLen int
lines []string
padder PadGrower
}
// NewAlign creates and initializes a ScanWriter with in and out as its initial Reader and Writer
// and sets del to the desired delimiter to be used for alignment.
// It is meant to read the contents of its io.Reader to determine the length of each field
// and output the results in an aligned format.
// Left Justification is used by default. See UpdatePadding to set the Justification.
func NewAlign(in io.Reader, out io.Writer, sep string, qu TextQualifier) *Align {
return &Align{
scanner: bufio.NewScanner(in),
writer: bufio.NewWriter(out),
sep: sep,
sepOut: sep,
columnCounts: make(map[int]int),
txtq: qu,
padOpts: PaddingOpts{
//defaults
Justification: JustifyLeft,
Pad: 1,
},
padder: &fieldPad{}, // default; set with UpdatePadder()
}
}
// OutputSep sets the output separator string with outsep if a different value from the input sep is desired.
func (a *Align) OutputSep(outsep string) {
a.sepOut = outsep
}
// Align determines the length of each field of text around the configured delimiter and aligns all of the
// text by the delimiter.
func (a *Align) Align() {
a.columnLength()
a.export()
}
// columnSize looks up the Align's columnCounts key with num and returns the value
// that was set by ColumnCounts().
// If num is not a valid key in Align.columnCounts, then -1 is returned.
func (a *Align) columnSize(num int) int {
if _, ok := a.columnCounts[num]; !ok {
return -1
}
return a.columnCounts[num]
}
// UpdatePadding uses PaddingOpts p to update the Align's padding options.
func (a *Align) UpdatePadding(p PaddingOpts) {
a.padOpts = p
}
// UpdatePadder sets the Align's padder implementation if a different
// one is desired from the default.
func (a *Align) UpdatePadder(padder PadGrower) {
a.padder = padder
}
// fieldLen works in a similar manner to the standard lib function strings.Index().
// Instead of returning the index of the first instance of sep, it returns the length
// of s before the first index of sep.
func fieldLen(s, sep string) int {
return genFieldLen(s, sep, "")
}
// fieldLenEscaped works in the same way as FieldLen, but a text qualifer string can
// be provided. If qual is an empty string, then the behavior will be exactly the same
// as FieldLen.
func fieldLenEscaped(s, sep, qual string) int {
return genFieldLen(s, sep, qual)
}
func genFieldLen(s, sep, qual string) int {
var endIdx int
if len(qual) > 0 && strings.HasPrefix(s, qual) {
endIdx += len(qual)
endIdx += strings.Index(s[endIdx:], qual) + len(qual)
return len(s[:endIdx])
}
endIdx += strings.Index(s, sep)
if endIdx == -1 {
// last field
return len(s)
}
return len(s[:endIdx])
}
// columnLength scans the input and determines the maximum length of each field based on
// the longest value for each field in all of the pertaining lines.
// All of the lines of the io.Reader are returned as a string slice.
func (a *Align) columnLength() {
a.lines = make([]string, 0)
for a.scanner.Scan() {
var columnNum int
var temp int
line := a.scanner.Text()
if a.txtq.On {
for start := 0; start < len(line); {
temp = fieldLenEscaped(line[start:], a.sep, a.txtq.Qualifier)
start += temp + len(a.sep)
if temp > a.columnCounts[columnNum] {
a.columnCounts[columnNum] = temp
}
columnNum++
temp = 0
}
} else {
for start := 0; start < len(line); {
temp = fieldLen(line[start:], a.sep)
start += temp + len(a.sep)
if temp > a.columnCounts[columnNum] {
a.columnCounts[columnNum] = temp
}
columnNum++
temp = 0
}
}
a.lines = append(a.lines, line)
}
}
const padchar byte = ' '
// export will pad each field in lines based on the Align's column counts.
func (a *Align) export() {
if a.padOpts.Pad < 0 {
a.padOpts.Pad = 0
}
surroundingPad := make([]byte, 0, a.padOpts.Pad)
for i := 0; i < a.padOpts.Pad; i++ {
surroundingPad = append(surroundingPad, padchar)
}
for _, line := range a.lines {
words := a.splitWithQual(line, a.sep, a.txtq.Qualifier)
var columnNum int
var tempColumn int // used for call to pad() to incorporate column filtering
for _, word := range words {
if a.filterLen > 0 {
if !contains(a.filter, columnNum+1) {
columnNum++
if columnNum == len(words) {
a.writer.WriteString("\n")
}
continue
}
}
j := a.padOpts.Justification
// override Justification for the specified columnNum in the key for the PaddingOpts.columnOverride map
if len(a.padOpts.ColumnOverride) > 0 {
for k, v := range a.padOpts.ColumnOverride {
if k == columnNum+1 {
j = v
}
}
}
padLength := countPadding(word, a.columnCounts[columnNum])
paddedWord := applyPadding(a.padder, word, string(surroundingPad), tempColumn, padLength, j)
a.padder.Reset() // empty the buffer for the next iteration.
columnNum++
tempColumn++
// Do not add a delimiter to the last field
// This also properly aligns the output even if there are lines with a different number of fields
if a.filterLen > 0 && a.filter[a.filterLen-1] == columnNum || columnNum == len(words) {
a.writer.Write(paddedWord)
a.writer.WriteByte('\n')
break
}
a.writer.Write(paddedWord)
a.writer.WriteString(a.sepOut)
}
}
a.writer.Flush()
}
func fillWithPadding(padder Padder, length int) {
for i := 0; i < length; i++ {
padder.WriteByte(padchar)
}
}
// applyPadding rebuilds word by adding padding appropriately based on the
// desired justification, the overall padding length and the supplied surrounding
// padding string.
func applyPadding(padder Padder, original, surroundingPad string, columnNum, padLength int, just Justification) []byte {
// add surrounding pad to beginning of column (except for the 1st column)
if len(surroundingPad) > 0 {
if columnNum > 0 {
padder.WriteString(surroundingPad)
}
}
switch just {
case JustifyLeft:
padder.WriteString(original)
fillWithPadding(padder, padLength)
case JustifyRight:
fillWithPadding(padder, padLength)
padder.WriteString(original)
case JustifyCenter:
// not much of a point to 'center' justification with such a small padding; default it if <= 2.
if padLength > 2 {
fillWithPadding(padder, (padLength - (padLength / 2)))
padder.WriteString(original)
fillWithPadding(padder, padLength/2)
} else {
padder.WriteString(original)
fillWithPadding(padder, padLength)
}
}
// add surrounding pad to end of column
if len(surroundingPad) > 0 {
padder.WriteString(surroundingPad)
}
return padder.Bytes()
}
// determines the length of the padding needed.
func countPadding(s string, count int) int {
padLength := count - len(s)
rCount, wordLen := runewidth.StringWidth(s), len(s)
if rCount < wordLen {
padLength += wordLen - rCount
}
return padLength
}
// prepends padding.
func leadingPad(sb *strings.Builder, padLen int) {
for i := 0; i < padLen; i++ {
sb.WriteByte(padchar)
}
}
// appends padding.
func trailingPad(sb *strings.Builder, padLen int) {
for i := 0; i < padLen; i++ {
sb.WriteByte(padchar)
}
}
// splitWithQual basically works like the standard strings.Split() func, but will consider a text qualifier if set.
func (a *Align) splitWithQual(s, sep, qual string) []string {
if !a.txtq.On {
return strings.Split(s, sep) // use standard Split() method if no qualifier is considered
}
var words = make([]string, 0, strings.Count(s, sep))
for start := 0; start <= len(s); {
count := genFieldLen(s[start:], sep, qual)
words = append(words, s[start:start+count])
start += count + len(sep)
}
return words
}
// FilterColumns sets which column numbers should be output.
func (a *Align) FilterColumns(c []int) {
a.filter = c
a.filterLen = len(c)
}
func contains(nums []int, num int) bool {
for _, v := range nums {
if v == num {
return true
}
}
return false
}