Skip to content

Commit febdc0a

Browse files
authored
Merge pull request #1156 from VXNCXNX/fix/leading-quoted-field-delimiter
fix: a delimiter inside a quoted field at line start splits the column
2 parents 949d0ca + 9db128a commit febdc0a

2 files changed

Lines changed: 134 additions & 1 deletion

File tree

oviewer/utils.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,40 @@ func allIndex(s string, substr string, reg *regexp.Regexp) [][]int {
6565
if reg != nil {
6666
return reg.FindAllStringIndex(s, -1)
6767
}
68-
return allStringIndex(s, substr)
68+
return allDelimiterIndex(s, substr)
69+
}
70+
71+
// allDelimiterIndex returns all delimiter positions.
72+
// A delimiter inside a double-quoted field is part of the field, not a
73+
// delimiter, including when the quoted field is the first one on the line.
74+
func allDelimiterIndex(s string, substr string) [][]int {
75+
if len(substr) == 0 {
76+
return nil
77+
}
78+
var result [][]int
79+
width := len(substr)
80+
offSet := 0
81+
if strings.HasPrefix(s, `"`) {
82+
s, offSet = skipQuoted(s, offSet)
83+
}
84+
for pos := strings.Index(s, substr); pos != -1; pos = strings.Index(s, substr) {
85+
s = s[pos+width:]
86+
result = append(result, []int{pos + offSet, pos + offSet + width})
87+
offSet += pos + width
88+
89+
if strings.HasPrefix(s, `"`) {
90+
s, offSet = skipQuoted(s, offSet)
91+
}
92+
}
93+
return result
94+
}
95+
96+
// skipQuoted skips the double-quoted field at the beginning of s and returns
97+
// the remainder with the updated offset. An unclosed quote skips only the
98+
// opening quote, which is what the delimiter search did before.
99+
func skipQuoted(s string, offSet int) (string, int) {
100+
qpos := strings.Index(s[1:], `"`)
101+
return s[qpos+2:], offSet + qpos + 2
69102
}
70103

71104
// allStringIndex returns all matching string positions.

oviewer/utils_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,106 @@ func Test_allStringIndex(t *testing.T) {
513513
}
514514
}
515515

516+
func Test_allDelimiterIndex(t *testing.T) {
517+
t.Parallel()
518+
type args struct {
519+
s string
520+
substr string
521+
}
522+
tests := []struct {
523+
name string
524+
args args
525+
want [][]int
526+
}{
527+
{
528+
name: "test1",
529+
args: args{
530+
s: "a,b,c",
531+
substr: ",",
532+
},
533+
want: [][]int{
534+
{1, 2},
535+
{3, 4},
536+
},
537+
},
538+
{
539+
name: "testNone",
540+
args: args{
541+
s: "a,b,c",
542+
substr: "@",
543+
},
544+
want: nil,
545+
},
546+
{
547+
name: "testNoSubstr",
548+
args: args{
549+
s: "a,b,c",
550+
substr: "",
551+
},
552+
want: nil,
553+
},
554+
{
555+
name: "testDoubleQuote",
556+
args: args{
557+
s: `a,"b,c",d`,
558+
substr: ",",
559+
},
560+
want: [][]int{
561+
{1, 2},
562+
{7, 8},
563+
},
564+
},
565+
{
566+
name: "testLeadingDoubleQuote",
567+
args: args{
568+
s: `"a,b",c,d`,
569+
substr: ",",
570+
},
571+
want: [][]int{
572+
{5, 6},
573+
{7, 8},
574+
},
575+
},
576+
{
577+
name: "testLeadingDoubleQuoteOnly",
578+
args: args{
579+
s: `"a,b"`,
580+
substr: ",",
581+
},
582+
want: nil,
583+
},
584+
{
585+
name: "testLeadingEmptyDoubleQuote",
586+
args: args{
587+
s: `"",a,b`,
588+
substr: ",",
589+
},
590+
want: [][]int{
591+
{2, 3},
592+
{4, 5},
593+
},
594+
},
595+
{
596+
name: "testLeadingUnclosedDoubleQuote",
597+
args: args{
598+
s: `"a,b`,
599+
substr: ",",
600+
},
601+
want: [][]int{
602+
{2, 3},
603+
},
604+
},
605+
}
606+
for _, tt := range tests {
607+
t.Run(tt.name, func(t *testing.T) {
608+
t.Parallel()
609+
if got := allDelimiterIndex(tt.args.s, tt.args.substr); !reflect.DeepEqual(got, tt.want) {
610+
t.Errorf("allDelimiterIndex() = %v, want %v", got, tt.want)
611+
}
612+
})
613+
}
614+
}
615+
516616
func Test_abs(t *testing.T) {
517617
t.Parallel()
518618
tests := []struct {

0 commit comments

Comments
 (0)