Line data Source code
1 : // Package util は、共通の軽量ユーティリティ関数群を提供します。
2 : // string.go は文字列関連の便利関数をまとめています
3 : package util
4 :
5 : import (
6 : "fmt"
7 : "strings"
8 : "unicode"
9 : )
10 :
11 : // Initialisms は Go の慣習に従って大文字で保持すべき単語のリストです。
12 : var Initialisms = map[string]bool{
13 : "ID": true,
14 : "JSON": true,
15 : "XML": true,
16 : "HTTP": true,
17 : "URL": true,
18 : "IP": true,
19 : }
20 :
21 : // ToHalfWidth は全角数字・英字・スペースを半角に変換します。
22 0 : func ToHalfWidth(s string) string {
23 0 : return toHalfWidth(s)
24 0 : }
25 :
26 : // toHalfWidth は全角数字・英字・スペースを半角に変換します。
27 0 : func toHalfWidth(s string) string {
28 0 : var b strings.Builder
29 0 : for _, r := range s {
30 0 : if r == 0x3000 { // 全角スペース
31 0 : _, _ = b.WriteRune(0x0020)
32 0 : } else if r >= 0xFF01 && r <= 0xFF5E { // 全角記号・英数字
33 0 : _, _ = b.WriteRune(r - 0xFEE0)
34 0 : } else {
35 0 : _, _ = b.WriteRune(r)
36 0 : }
37 : }
38 0 : return b.String()
39 : }
40 :
41 : // OptPtr は空文字列なら nil、非空なら *string を返します。
42 : // JSONやDBにNULLを入れたいときなどに便利です。
43 0 : func OptPtr(s string) *string {
44 0 : if s == "" {
45 0 : return nil
46 0 : }
47 0 : return &s
48 : }
49 :
50 : // SafeStr は、将来のnil対応などを見越した「安全な文字列取得」用のラッパ。
51 : // 現状はそのまま返しますが、nilガード付きのCloneなどと統一しておくと保守性が上がります。
52 0 : func SafeStr(s string) string {
53 0 : return s
54 0 : }
55 :
56 : // FallbackStr は、最初に空でない文字列を返します。
57 : // すべて空の場合は空文字列("")を返します。
58 : //
59 : // 例:
60 : //
61 : // name := util.FallbackStr(p.DisplayName, user.DisplayName, "unknown")
62 0 : func FallbackStr(ss ...string) string {
63 0 : for _, s := range ss {
64 0 : if s != "" {
65 0 : return s
66 0 : }
67 : }
68 0 : return ""
69 : }
70 :
71 : // ToCamelLower は ToCamel へ統合したため削除(呼び出し側は ToCamel に置換
72 : // ToCamelLower は、与えられた文字列の先頭文字を小文字に変換して返します。
73 : // 例: "UserName" → "userName"。
74 : // 空文字列を渡した場合はそのまま空文字を返します。
75 : //func ToCamelLower(s string) string {
76 : // if s == "" {
77 : // return s
78 : // }
79 : // return strings.ToLower(s[:1]) + s[1:]
80 : //}
81 :
82 : // normalizeToSnake は、入力が snake/kebab/Pascal/camel/UPPER でも
83 : // いったん snake_case(小文字)へ正規化する内部関数。
84 0 : func normalizeToSnake(s string) string {
85 0 : if s == "" {
86 0 : return s
87 0 : }
88 : // kebab → snake
89 0 : s = strings.ReplaceAll(s, "-", "_")
90 0 : // Pascal/camel → snake(大文字手前に "_" を挿入)
91 0 : var out []rune
92 0 : var prev rune
93 0 : for i, r := range s {
94 0 : if r == '_' {
95 0 : out = append(out, r)
96 0 : prev = r
97 0 : continue
98 : }
99 : // 前が 英小文字/数字、今が 英大文字 → 境界
100 0 : if i > 0 && unicode.IsUpper(r) && (unicode.IsLower(prev) || unicode.IsDigit(prev)) {
101 0 : out = append(out, '_')
102 0 : }
103 : // 連続大文字 → 次が小文字なら境界(HTTPServer → http_server)
104 0 : if i > 0 && unicode.IsUpper(r) && unicode.IsUpper(prev) {
105 0 : // 次を先読み
106 0 : if i+1 < len(s) {
107 0 : n := rune(s[i+1])
108 0 : if unicode.IsLower(n) {
109 0 : out = append(out, '_')
110 0 : }
111 : }
112 : }
113 0 : out = append(out, unicode.ToLower(r))
114 0 : prev = r
115 : }
116 : // 連続区切りの正規化("__" 等): Split 時に無視されるのでここではそのままでもOK
117 0 : return string(out)
118 : }
119 :
120 : // ToSnake は、入力が何であっても snake_case に変換します。
121 0 : func ToSnake(s string) string {
122 0 : return normalizeToSnake(s)
123 0 : }
124 :
125 : // ToKebab は、入力が何であっても kebab-case に変換します。
126 0 : func ToKebab(s string) string {
127 0 : if s == "" {
128 0 : return s
129 0 : }
130 0 : return strings.ReplaceAll(normalizeToSnake(s), "_", "-")
131 : }
132 :
133 : // ToPascal は、入力が何であっても PascalCase に変換します。
134 0 : func ToPascal(s string) string {
135 0 : if s == "" {
136 0 : return s
137 0 : }
138 0 : parts := strings.Split(normalizeToSnake(s), "_")
139 0 : for i := range parts {
140 0 : if parts[i] == "" {
141 0 : continue
142 : }
143 0 : upper := strings.ToUpper(parts[i])
144 0 : if Initialisms[upper] {
145 0 : parts[i] = upper
146 0 : } else {
147 0 : parts[i] = strings.ToUpper(parts[i][:1]) + strings.ToLower(parts[i][1:])
148 0 : }
149 : }
150 0 : return strings.Join(parts, "")
151 : }
152 :
153 : // ToCamel は入力文字列を lowerCamelCase に変換します。
154 : // snake_case, kebab-case, PascalCase, UPPER_CASE いずれにも対応。
155 : // 例:
156 : //
157 : // "jp_pref" → "jpPref"
158 : // "postal-code" → "postalCode"
159 : // "PostalCode" → "postalCode"
160 : // "JP_PREF" → "jpPref"
161 : // "jpPref" → "jpPref" (そのまま)
162 0 : func ToCamel(s string) string {
163 0 : if s == "" {
164 0 : return s
165 0 : }
166 :
167 : // 1) まず snake に正規化
168 0 : parts := strings.Split(normalizeToSnake(s), "_")
169 0 :
170 0 : for i := range parts {
171 0 : if parts[i] == "" {
172 0 : continue
173 : }
174 0 : if i > 0 {
175 0 : upper := strings.ToUpper(parts[i])
176 0 : if Initialisms[upper] {
177 0 : parts[i] = upper
178 0 : } else {
179 0 : parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
180 0 : }
181 0 : } else {
182 0 : parts[i] = strings.ToLower(parts[i])
183 0 : }
184 : }
185 0 : return strings.Join(parts, "")
186 :
187 : }
188 :
189 : // Humanize は表記ゆれ(snake/kebab/camel/Pascal/UPPER)を問わず
190 : // 「先頭だけ大文字 + スペース区切り」の人間可読表記に変換します。
191 : // 例:
192 : // - "postalCode" → "Postal code"
193 : // - "address_line1" → "Address line1"
194 : // - "HTTPServerID" → "Http server id"
195 : // - "jp-pref" → "Jp pref"
196 : // - "JP_PREF" → "Jp pref"
197 0 : func Humanize(s string) string {
198 0 : if s == "" {
199 0 : return s
200 0 : }
201 : // 1) まず snake_case に正規化(小文字化もされる)
202 0 : s = normalizeToSnake(s)
203 0 :
204 0 : // 2) "_" → " " に置換(連続区切りは自然に連続スペースになり得る)
205 0 : s = strings.ReplaceAll(s, "_", " ")
206 0 :
207 0 : // 3) 余分なスペースの整理(前後・連続)
208 0 : s = strings.TrimSpace(s)
209 0 : // 連続スペースを 1 個に
210 0 : var b strings.Builder
211 0 : prevSpace := false
212 0 : for _, r := range s {
213 0 : if r == ' ' {
214 0 : if !prevSpace {
215 0 : _, _ = b.WriteRune(' ')
216 0 : prevSpace = true
217 0 : }
218 0 : continue
219 : }
220 0 : _, _ = b.WriteRune(r)
221 0 : prevSpace = false
222 : }
223 0 : s = b.String()
224 0 :
225 0 : if s == "" {
226 0 : return s
227 0 : }
228 :
229 : // 4) 先頭だけ大文字に(他はすでに小文字化済みなのでそのまま)
230 : // 例: "http server id" → "Http server id"
231 0 : runes := []rune(s)
232 0 : runes[0] = unicode.ToUpper(runes[0])
233 0 : return string(runes)
234 : }
235 :
236 : // Fmt は fmt.Sprintf の簡略ラッパです。
237 : // フォーマット文字列をより簡潔に記述したい場合に使用します。
238 : //
239 : // 例:
240 : //
241 : // util.Fmt("ui.profile.age_group.%d", 20) → "ui.profile.age_group.20"
242 0 : func Fmt(format string, args ...any) string {
243 0 : return fmt.Sprintf(format, args...)
244 0 : }
245 :
246 : // NullableString は、空文字列の場合 nil を返し、
247 : // 非空文字列の場合はポインタを返すユーティリティです。
248 0 : func NullableString(s string) *string {
249 0 : if s == "" {
250 0 : return nil
251 0 : }
252 0 : return &s
253 : }
254 :
255 : // DerefString は、nil ポインタの場合は "" を返し、
256 : // 値があればその内容を返す逆変換です。
257 0 : func DerefString(s *string) string {
258 0 : if s == nil {
259 0 : return ""
260 0 : }
261 0 : return *s
262 : }
263 :
264 : // ToCanonical は、名寄せや比較のために文字列を正規化します。
265 : // 入力が camelCase, snake_case, PascalCase, kebab-case,
266 : // あるいは不規則な空白混じりであっても、すべて「小文字の半角スペース区切り」に統一します。
267 0 : func ToCanonical(s string) string {
268 0 : if s == "" {
269 0 : return s
270 0 : }
271 :
272 : // 0) 全角英数字・記号を半角に
273 0 : s = toHalfWidth(s)
274 0 :
275 0 : // 1) 既存の normalizeToSnake(s) を利用
276 0 : // これにより以下の変換が一度に行われます:
277 0 : // - "AdobePhotoshop" (camel/Pascal) -> "adobe_photoshop"
278 0 : // - "ADOBE_PHOTOSHOP" (UPPER) -> "adobe_photoshop"
279 0 : // - "adobe-photoshop" (kebab) -> "adobe_photoshop"
280 0 : // - "Adobe" -> "adobe"
281 0 : snaked := normalizeToSnake(s)
282 0 :
283 0 : // 2) アンダースコアを半角スペースに置換
284 0 : spaced := strings.ReplaceAll(snaked, "_", " ")
285 0 :
286 0 : // 3) strings.Fields で「連続する空白」を完全に除去して分割
287 0 : // 4) strings.Join で「単一の半角スペース」で結合
288 0 : parts := strings.Fields(spaced)
289 0 : return strings.Join(parts, " ")
290 : }
|