├── go.mod ├── tmp └── main ├── purse_test.go ├── LICENSE └── purse.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/phillip-england/purse 2 | 3 | go 1.23.3 4 | -------------------------------------------------------------------------------- /tmp/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phillip-England/purse/main/tmp/main -------------------------------------------------------------------------------- /purse_test.go: -------------------------------------------------------------------------------- 1 | package purse_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMain(t *testing.T) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Phillip England 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /purse.go: -------------------------------------------------------------------------------- 1 | // Package purse provides utility functions for string manipulation and slice handling. 2 | package purse 3 | 4 | import ( 5 | "fmt" 6 | "math/rand" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // MakeLines splits a string into lines. 12 | func MakeLines(s string) []string { 13 | return strings.Split(s, "\n") 14 | } 15 | 16 | // JoinLines joins an array of strings into a single string with newlines. 17 | func JoinLines(lines []string) string { 18 | return strings.Join(lines, "\n") 19 | } 20 | 21 | // ReplaceLastSubStr replaces the last occurrence of a substring in a string. 22 | func ReplaceLastSubStr(s, old, new string) string { 23 | pos := strings.LastIndex(s, old) 24 | if pos == -1 { 25 | return s 26 | } 27 | return s[:pos] + new + s[pos+len(old):] 28 | } 29 | 30 | // GetFirstLine returns the first line of a string. 31 | func GetFirstLine(s string) string { 32 | lines := MakeLines(s) 33 | if len(lines) == 0 { 34 | return s 35 | } 36 | return lines[0] 37 | } 38 | 39 | // GetLastLine returns the last line of a string. 40 | func GetLastLine(s string) string { 41 | lines := MakeLines(s) 42 | if len(lines) == 0 { 43 | return s 44 | } 45 | return lines[len(lines)-1] 46 | } 47 | 48 | // RemoveAllSubStr removes all specified substrings from a string. 49 | func RemoveAllSubStr(s string, subs ...string) string { 50 | for _, sub := range subs { 51 | s = strings.ReplaceAll(s, sub, "") 52 | } 53 | return s 54 | } 55 | 56 | // CountLeadingSpaces counts the number of leading spaces in a string. 57 | func CountLeadingSpaces(line string) int { 58 | count := 0 59 | for _, char := range line { 60 | if char != ' ' { 61 | break 62 | } 63 | count++ 64 | } 65 | return count 66 | } 67 | 68 | // PrefixLines adds a prefix to each line of a string. 69 | func PrefixLines(str, prefix string) string { 70 | lines := strings.Split(str, "\n") 71 | for i, line := range lines { 72 | lines[i] = prefix + line 73 | } 74 | return strings.Join(lines, "\n") 75 | } 76 | 77 | // FlattenLines removes leading spaces and tabs from each line of a slice. 78 | func FlattenLines(lines []string) []string { 79 | for i, line := range lines { 80 | lines[i] = strings.TrimLeft(line, " \t") 81 | } 82 | return lines 83 | } 84 | 85 | // Flatten removes leading spaces and tabs from all lines of a string. 86 | func Flatten(str string) string { 87 | lines := MakeLines(str) 88 | flat := FlattenLines(lines) 89 | return strings.Join(flat, "") 90 | } 91 | 92 | // TrimLeadingSpaces removes leading spaces from all lines of a string. 93 | func TrimLeadingSpaces(str string) string { 94 | lines := strings.Split(str, "\n") 95 | for i, line := range lines { 96 | lines[i] = strings.TrimLeft(line, " ") 97 | } 98 | return strings.Join(lines, "\n") 99 | } 100 | 101 | func TrimLeadingTabs(str string) string { 102 | lines := strings.Split(str, "\n") 103 | for i, line := range lines { 104 | lines[i] = strings.TrimLeft(line, "\t") 105 | } 106 | return strings.Join(lines, "\n") 107 | } 108 | 109 | func TrimSomeLeadingTabs(str string, tabsToTrim int) string { 110 | lines := strings.Split(str, "\n") 111 | for i, line := range lines { 112 | trimmed := 0 113 | for j, char := range line { 114 | if char == '\t' && trimmed < tabsToTrim { 115 | trimmed++ 116 | } else { 117 | lines[i] = line[j:] // Trim up to the number of tabs specified 118 | break 119 | } 120 | } 121 | // If the line only contained tabs, ensure it's set correctly 122 | if trimmed > 0 && strings.TrimLeft(line, "\t") == "" { 123 | lines[i] = "" 124 | } 125 | } 126 | return strings.Join(lines, "\n") 127 | } 128 | 129 | // SliceContains checks if a slice contains a specific item. 130 | func SliceContains(slice []string, item string) bool { 131 | for _, s := range slice { 132 | if s == item { 133 | return true 134 | } 135 | } 136 | return false 137 | } 138 | 139 | // BackTick returns a backtick character. 140 | func BackTick() string { 141 | return "`" 142 | } 143 | 144 | // ReplaceFirstLine replaces the first line of a string with a new line. 145 | func ReplaceFirstLine(input, newLine string) string { 146 | lines := strings.Split(input, "\n") 147 | if len(lines) > 0 { 148 | lines[0] = newLine 149 | } 150 | return strings.Join(lines, "\n") 151 | } 152 | 153 | // ReplaceLastLine replaces the last line of a string with a new line. 154 | func ReplaceLastLine(input, newLine string) string { 155 | lines := strings.Split(input, "\n") 156 | if len(lines) > 0 { 157 | lines[len(lines)-1] = newLine 158 | } 159 | return strings.Join(lines, "\n") 160 | } 161 | 162 | // Squeeze removes all spaces from a string. 163 | func Squeeze(s string) string { 164 | return strings.ReplaceAll(s, " ", "") 165 | } 166 | 167 | // ScanBetweenSubStrs extracts substrings between specified delimiters. 168 | func ScanBetweenSubStrs(s, start, end string) []string { 169 | var out []string 170 | inSearch := false 171 | searchStr := "" 172 | i := 0 173 | for i < len(s) { 174 | if !inSearch && i+len(start) <= len(s) && s[i:i+len(start)] == start { 175 | inSearch = true 176 | searchStr = start 177 | i += len(start) 178 | continue 179 | } 180 | if inSearch { 181 | if i+len(end) <= len(s) && s[i:i+len(end)] == end { 182 | searchStr += end 183 | out = append(out, searchStr) 184 | searchStr = "" 185 | inSearch = false 186 | i += len(end) 187 | continue 188 | } 189 | searchStr += string(s[i]) 190 | } 191 | i++ 192 | } 193 | return out 194 | } 195 | 196 | // RemoveFirstLine removes the first line from a string. 197 | func RemoveFirstLine(input string) string { 198 | index := strings.Index(input, "\n") 199 | if index == -1 { 200 | return "" 201 | } 202 | return input[index+1:] 203 | } 204 | 205 | // RemoveTrailingEmptyLines removes empty lines from the end of a string. 206 | func RemoveTrailingEmptyLines(input string) string { 207 | lines := strings.Split(input, "\n") 208 | for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" { 209 | lines = lines[:len(lines)-1] 210 | } 211 | return strings.Join(lines, "\n") 212 | } 213 | 214 | // RemoveEmptyLines removes all empty lines from a string. 215 | func RemoveEmptyLines(input string) string { 216 | lines := strings.Split(input, "\n") 217 | var cleanedLines []string 218 | for _, line := range lines { 219 | if strings.TrimSpace(line) != "" { 220 | cleanedLines = append(cleanedLines, line) 221 | } 222 | } 223 | return strings.Join(cleanedLines, "\n") 224 | } 225 | 226 | // RemoveDuplicatesInSlice removes duplicate items from a slice. 227 | func RemoveDuplicatesInSlice(strSlice []string) []string { 228 | unique := make(map[string]bool) 229 | var result []string 230 | for _, item := range strSlice { 231 | if _, found := unique[item]; !found { 232 | unique[item] = true 233 | result = append(result, item) 234 | } 235 | } 236 | return result 237 | } 238 | 239 | // WrapStr wraps a string with a prefix and a suffix. 240 | func WrapStr(s, prefix, suffix string) string { 241 | return prefix + s + suffix 242 | } 243 | 244 | // MatchLeadingSpaces matches the leading spaces of one string to another. 245 | func MatchLeadingSpaces(str1, str2 string) string { 246 | leadingSpaces := len(str2) - len(strings.TrimLeft(str2, " ")) 247 | padding := strings.Repeat(" ", leadingSpaces) 248 | return padding + str1 249 | } 250 | 251 | // SnipStrAtIndex truncates a string at a given index. 252 | func SnipStrAtIndex(s string, x int) string { 253 | if x > len(s) { 254 | x = len(s) 255 | } 256 | return s[:x] 257 | } 258 | 259 | // TargetSearch finds a substring between two specified strings. 260 | func TargetSearch(input, primarySearch, secondarySearch string) (string, bool) { 261 | startIndex := strings.Index(input, primarySearch) 262 | if startIndex == -1 { 263 | return "", false 264 | } 265 | substring := input[startIndex:] 266 | endIndex := strings.Index(substring, secondarySearch) 267 | if endIndex == -1 { 268 | return "", false 269 | } 270 | finalEndIndex := startIndex + endIndex + len(secondarySearch) 271 | return input[startIndex:finalEndIndex], true 272 | } 273 | 274 | // SplitWithTargetInclusion splits a string and includes the target as separate items. 275 | func SplitWithTargetInclusion(str, target string) []string { 276 | var parts []string 277 | start := 0 278 | for { 279 | index := strings.Index(str[start:], target) 280 | if index == -1 { 281 | parts = append(parts, str[start:]) 282 | break 283 | } 284 | index += start 285 | parts = append(parts, str[start:index]) 286 | parts = append(parts, target) 287 | start = index + len(target) 288 | } 289 | return parts 290 | } 291 | 292 | // PrefixSliceItems prefixes each item in a slice with a string. 293 | func PrefixSliceItems(items []string, prefix string) string { 294 | var prefixedItems []string 295 | for _, item := range items { 296 | prefixedItems = append(prefixedItems, prefix+item) 297 | } 298 | return strings.Join(prefixedItems, "") 299 | } 300 | 301 | // ReverseSlice reverses the order of elements in a slice. 302 | func ReverseSlice[T any](slice []T) []T { 303 | n := len(slice) 304 | reversed := make([]T, n) 305 | for i, v := range slice { 306 | reversed[n-1-i] = v 307 | } 308 | return reversed 309 | } 310 | 311 | // RandStr generates a random string of specified length. 312 | func RandStr(length int) string { 313 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 314 | rand.Seed(time.Now().UnixNano()) 315 | b := make([]byte, length) 316 | for i := range b { 317 | b[i] = charset[rand.Intn(len(charset))] 318 | } 319 | return string(b) 320 | } 321 | 322 | // MustEqualOneOf checks if a string matches any of the provided options. 323 | func MustEqualOneOf(str string, options ...string) bool { 324 | for _, option := range options { 325 | if str == option { 326 | return true 327 | } 328 | } 329 | return false 330 | } 331 | 332 | // ReplaceFirstInstanceOf replaces the first occurrence of `old` with `new` in `s`. 333 | func ReplaceFirstInstanceOf(s, old, new string) string { 334 | index := strings.Index(s, old) 335 | if index == -1 { 336 | return s // Return the original string if the substring is not found 337 | } 338 | return s[:index] + new + s[index+len(old):] 339 | } 340 | 341 | // ReplaceLastInstanceOf replaces the last occurrence of `old` with `new` in `s`. 342 | func ReplaceLastInstanceOf(s, old, new string) string { 343 | index := strings.LastIndex(s, old) 344 | if index == -1 { 345 | return s // Return the original string if the substring is not found 346 | } 347 | return s[:index] + new + s[index+len(old):] 348 | } 349 | 350 | // Split a string by " " spaces and work on each chunck 351 | func WorkOnStrChunks(input string, processFunc func(string) error) error { 352 | // Split the input string by spaces 353 | chunks := strings.Fields(input) 354 | 355 | // Iterate over each chunk 356 | for _, chunk := range chunks { 357 | // Apply the provided function to the chunk 358 | if err := processFunc(chunk); err != nil { 359 | return fmt.Errorf("error processing chunk %q: %w", chunk, err) 360 | } 361 | } 362 | 363 | return nil 364 | } 365 | 366 | func KebabToCamelCase(input string) string { 367 | parts := strings.Split(input, "-") 368 | if len(parts) == 0 { 369 | return "" 370 | } 371 | for i := 1; i < len(parts); i++ { 372 | if len(parts[i]) > 0 { 373 | parts[i] = strings.ToUpper(string(parts[i][0])) + strings.ToLower(parts[i][1:]) 374 | } 375 | } 376 | return strings.Join(parts, "") 377 | } 378 | 379 | func FindMatchInStrSlice(slice []string, str string) string { 380 | for _, item := range slice { 381 | if item == str { 382 | return item 383 | } 384 | } 385 | return "" 386 | } 387 | 388 | func GetAllLetters() []string { 389 | letters := make([]string, 0, 26*2) // 26 lowercase + 26 uppercase 390 | for ch := 'a'; ch <= 'z'; ch++ { 391 | letters = append(letters, string(ch)) 392 | } 393 | for ch := 'A'; ch <= 'Z'; ch++ { 394 | letters = append(letters, string(ch)) 395 | } 396 | return letters 397 | } 398 | 399 | func GetAllUpperCaseLetters() []string { 400 | letters := make([]string, 0, 26*2) // 26 lowercase + 26 uppercase 401 | for ch := 'A'; ch <= 'Z'; ch++ { 402 | letters = append(letters, string(ch)) 403 | } 404 | return letters 405 | } 406 | 407 | func GetAllLowerCaseLetters() []string { 408 | letters := make([]string, 0, 26*2) // 26 lowercase + 26 uppercase 409 | for ch := 'a'; ch <= 'z'; ch++ { 410 | letters = append(letters, string(ch)) 411 | } 412 | return letters 413 | } 414 | 415 | func GetAllNumbers() []string { 416 | numbers := make([]string, 0, 10) // Digits 0-9 417 | for ch := '0'; ch <= '9'; ch++ { 418 | numbers = append(numbers, string(ch)) 419 | } 420 | return numbers 421 | } 422 | 423 | func EnforeWhitelist(input string, whitelist []string) bool { 424 | for _, char := range input { 425 | if !contains(whitelist, string(char)) { 426 | return false 427 | } 428 | } 429 | return true 430 | } 431 | 432 | func contains(slice []string, str string) bool { 433 | for _, item := range slice { 434 | if item == str { 435 | return true 436 | } 437 | } 438 | return false 439 | } 440 | 441 | func Err(template string, args ...any) error { 442 | formatted := Fmt(template, args...) 443 | return fmt.Errorf(formatted) 444 | } 445 | 446 | func Fmt(str string, args ...any) string { 447 | lines := MakeLines(str) 448 | if len(lines) == 0 { 449 | return "" 450 | } 451 | firstLine := lines[0] 452 | sq := Squeeze(firstLine) 453 | if sq == "" { 454 | str = RemoveFirstLine(str) 455 | } 456 | lines = MakeLines(str) 457 | if len(lines) == 0 { 458 | return "" 459 | } 460 | firstLine = lines[0] 461 | firstLineTabs := CountLeadingTabs(firstLine) 462 | out := make([]string, 0) 463 | for i, line := range lines { 464 | line = TrimSomeLeadingTabs(line, firstLineTabs) 465 | if i == len(lines)-1 && Squeeze(line) == "" { 466 | continue 467 | } 468 | out = append(out, line) 469 | } 470 | return fmt.Sprintf(strings.Join(out, "\n"), args...) 471 | } 472 | 473 | func RemoveWrappingQuotes(s string) string { 474 | if len(s) >= 2 { 475 | first, last := s[0], s[len(s)-1] 476 | if (first == '\'' && last == '\'') || (first == '"' && last == '"') { 477 | return s[1 : len(s)-1] 478 | } 479 | } 480 | return s 481 | } 482 | 483 | func EnforceBlacklist(input string, blacklist []string) bool { 484 | for _, char := range input { 485 | if contains(blacklist, string(char)) { 486 | return false 487 | } 488 | } 489 | return true 490 | } 491 | 492 | // IsQuoteValid checks if a string contains balanced and valid quotes. 493 | func IsQuoteValid(s string) bool { 494 | doubleQuoteOpen := false 495 | singleQuoteOpen := false 496 | for _, char := range s { 497 | switch char { 498 | case '"': 499 | if singleQuoteOpen { 500 | return false 501 | } 502 | doubleQuoteOpen = !doubleQuoteOpen 503 | case '\'': 504 | if doubleQuoteOpen { 505 | return false 506 | } 507 | singleQuoteOpen = !singleQuoteOpen 508 | } 509 | } 510 | return !doubleQuoteOpen && !singleQuoteOpen 511 | } 512 | 513 | func CountLeadingTabs(line string) int { 514 | count := 0 515 | for _, char := range line { 516 | if string(char) != "\t" { 517 | break 518 | } 519 | count++ 520 | } 521 | return count 522 | } 523 | --------------------------------------------------------------------------------