aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/ettle/strcase/split.go
blob: 84381106bc698065e5a784d5d61090bd7fc80eab (plain)
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
package strcase

import "unicode"

// SplitFn defines how to split a string into words
type SplitFn func(prev, curr, next rune) SplitAction

// NewSplitFn returns a SplitFn based on the options provided.
//
// NewSplitFn covers the majority of common options that other strcase
// libraries provide and should allow you to simply create a custom caser.
// For more complicated use cases, feel free to write your own SplitFn
//nolint:gocyclo
func NewSplitFn(
	delimiters []rune,
	splitOptions ...SplitOption,
) SplitFn {
	var splitCase, splitAcronym, splitBeforeNumber, splitAfterNumber, preserveNumberFormatting bool

	for _, option := range splitOptions {
		switch option {
		case SplitCase:
			splitCase = true
		case SplitAcronym:
			splitAcronym = true
		case SplitBeforeNumber:
			splitBeforeNumber = true
		case SplitAfterNumber:
			splitAfterNumber = true
		case PreserveNumberFormatting:
			preserveNumberFormatting = true
		}
	}

	return func(prev, curr, next rune) SplitAction {
		// The most common case will be that it's just a letter
		// There are safe cases to process
		if isLower(curr) && !isNumber(prev) {
			return Noop
		}
		if isUpper(prev) && isUpper(curr) && isUpper(next) {
			return Noop
		}

		if preserveNumberFormatting {
			if (curr == '.' || curr == ',') &&
				isNumber(prev) && isNumber(next) {
				return Noop
			}
		}

		if unicode.IsSpace(curr) {
			return SkipSplit
		}
		for _, d := range delimiters {
			if curr == d {
				return SkipSplit
			}
		}

		if splitBeforeNumber {
			if isNumber(curr) && !isNumber(prev) {
				if preserveNumberFormatting && (prev == '.' || prev == ',') {
					return Noop
				}
				return Split
			}
		}

		if splitAfterNumber {
			if isNumber(prev) && !isNumber(curr) {
				return Split
			}
		}

		if splitCase {
			if !isUpper(prev) && isUpper(curr) {
				return Split
			}
		}

		if splitAcronym {
			if isUpper(prev) && isUpper(curr) && isLower(next) {
				return Split
			}
		}

		return Noop
	}
}

// SplitOption are options that allow for configuring NewSplitFn
type SplitOption int

const (
	// SplitCase - FooBar -> Foo_Bar
	SplitCase SplitOption = iota
	// SplitAcronym - FOOBar -> Foo_Bar
	// It won't preserve FOO's case. If you want, you can set the Caser's initialisms so FOO will be in all caps
	SplitAcronym
	// SplitBeforeNumber - port80 -> port_80
	SplitBeforeNumber
	// SplitAfterNumber - 200status -> 200_status
	SplitAfterNumber
	// PreserveNumberFormatting - a.b.2,000.3.c -> a_b_2,000.3_c
	PreserveNumberFormatting
)

// SplitAction defines if and how to split a string
type SplitAction int

const (
	// Noop - Continue to next character
	Noop SplitAction = iota
	// Split - Split between words
	// e.g. to split between wordsWithoutDelimiters
	Split
	// SkipSplit - Split the word and drop the character
	// e.g. to split words with delimiters
	SkipSplit
	// Skip - Remove the character completely
	Skip
)

//nolint:gocyclo
func defaultSplitFn(prev, curr, next rune) SplitAction {
	// The most common case will be that it's just a letter so let lowercase letters return early since we know what they should do
	if isLower(curr) {
		return Noop
	}
	// Delimiters are _, -, ., and unicode spaces
	// Handle . lower down as it needs to happen after number exceptions
	if curr == '_' || curr == '-' || isSpace(curr) {
		return SkipSplit
	}

	if isUpper(curr) {
		if isLower(prev) {
			// fooBar
			return Split
		} else if isUpper(prev) && isLower(next) {
			// FOOBar
			return Split
		}
	}

	// Do numeric exceptions last to avoid perf penalty
	if unicode.IsNumber(prev) {
		// v4.3 is not split
		if (curr == '.' || curr == ',') && unicode.IsNumber(next) {
			return Noop
		}
		if !unicode.IsNumber(curr) && curr != '.' {
			return Split
		}
	}
	// While period is a default delimiter, keep it down here to avoid
	// penalty for other delimiters
	if curr == '.' {
		return SkipSplit
	}

	return Noop
}