Skip to content

Commit

Permalink
Enhancements to LibStringy
Browse files Browse the repository at this point in the history
  • Loading branch information
hishma committed Mar 6, 2021
1 parent f8ca985 commit c02083f
Show file tree
Hide file tree
Showing 11 changed files with 540 additions and 13 deletions.
126 changes: 125 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,127 @@
# Stringy

A description of this package.
Handy string conversions

## Command Line Tool

A Mac OS command line tool to perform some handy string conversions

### Usage

```
OVERVIEW: Handy string conversions
USAGE: stringy <subcommand>
OPTIONS:
-h, --help Show help information.
SUBCOMMANDS:
camelcase Converts strings to camelCase
snakecase Converts strings to snake_case.
kebabcase Converts strings to kebab-case.
See 'stringy help <subcommand>' for detailed help.
```

### Subcommands

#### camelcase

```
OVERVIEW: Converts strings to camelCase
E.G. Earth Sun Moon -> earthSunMoon
USAGE: stringy camelcase [--invert] [<strings> ...]
ARGUMENTS:
<strings>
OPTIONS:
-i, --invert Inverts the conversion.
-h, --help Show help information.
```

#### snakecase

```
OVERVIEW: Converts strings to snake_case.
E.G. 'Snakes are slithery' -> 'snakes_are_slithery'
USAGE: stringy snakecase [--invert] [<strings> ...]
ARGUMENTS:
<strings>
OPTIONS:
-i, --invert Inverts the conversion.
-h, --help Show help information.
```

#### snakecase

```
OVERVIEW: Converts strings to kebab-case.
E.G. 'Words on a stick' -> 'words-on-a-stick'
USAGE: stringy kebabcase [--invert] [<strings> ...]
ARGUMENTS:
<strings>
OPTIONS:
-i, --invert Inverts the conversion.
-h, --help Show help information.
```

### Installation


#### With [`Mint`](https://github.com/yonaskolb/Mint)

```sh
$ mint install salishseasoftware/stringy
```


#### Manually

Clone the repo then:

```
$ make install
```

Or using swift itself:

```
$ swift build -c release
$ cp .build/release/stringy /usr/local/bin/stringy
```

#### With Xcode

Generate the Xcode project:

```
$ swift package generate-xcodeproj
$ open ./Stringy.xcodeproj
```

In Xcode:

1. Product > Archive
1. Distribute Content
1. Built Products
1. copy `stringy` executable to `/usr/local/bin/` or wherever you prefer.


## LibStringy

Swift package of handy String extensions.



83 changes: 83 additions & 0 deletions Sources/LibStringy/String+DataDetector.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import Foundation

public extension String {

// MARK: - Web URL

/// Extract all web urls from a string.
func webURLs() -> [String] {
guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
return []
}
let matches = detector.matches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count))

return matches.compactMap {
guard let url = $0.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
components.url != nil,
["http", "https"].contains(components.scheme)
else {
return nil
}
return url.absoluteString.isEmpty ? nil : url.absoluteString
}
}

/// True if a string is a valid web URL, according to apple's data detectors anyway.
var isWebUrl: Bool {
let urls = self.webURLs()
guard urls.count == 1, let found = urls.first else { return false }
return found == self
}

// MARK: - Email address

/// Extract all email addresses from a string.
func emailAddresses() -> [String] {
guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
return []
}
let matches = detector.matches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count))

return matches.compactMap {
guard let url = $0.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
components.scheme == "mailto"
else {
return nil
}
return components.path
}
}

/// True if a string is a valid email address, according to apple's data detectors anyway.
///
/// Friends don't let friends use regex to validate email addresses
///
/// * https://stackoverflow.com/questions/48055431/can-it-cause-harm-to-validate-email-addresses-with-a-regex
/// * https://stackoverflow.com/questions/201323/how-to-validate-an-email-address-using-a-regular-expression
///
var isEmailAddress: Bool {
let emails = self.emailAddresses()
guard emails.count == 1, let found = emails.first else { return false }
return found == self
}

// MARK: - Phone number

/// True if a string is a valid phone number, according to apple's data detectors anyway.
var isPhoneNumber: Bool {
let phoneNumbers = self.phoneNumbers()
guard phoneNumbers.count == 1, let found = phoneNumbers.first else { return false }
return found == self
}

/// Extract all phone numbers from a string.
func phoneNumbers() -> [String] {
guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.phoneNumber.rawValue) else {
return []
}
let matches = detector.matches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count))
return matches.compactMap { $0.phoneNumber }
}
}
5 changes: 5 additions & 0 deletions Sources/LibStringy/String+Error.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

extension String: Error, LocalizedError {
public var errorDescription: String? { return self }
}
35 changes: 35 additions & 0 deletions Sources/LibStringy/String.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

public extension String {
/// Adds prefix to self
mutating func addPrefix(_ prefix: String) {
self = prefix + self
}

/// Returns a new string, with the prefix added
func addingPrefix(_ prefix: String) -> String {
return prefix + self
}

/// Adds a given prefix to self, if the prefix does exist in self.
mutating func addPrefixIfNeeded(_ prefix: String) {
guard !self.hasPrefix(prefix) else { return }
self = prefix + self
}

/// Returns a new string, with the prefix added if does not already exist.
func addingPrefixIfNeeded(_ prefix: String) -> String {
guard !self.hasPrefix(prefix) else { return self }
return prefix + self
}
}

public extension Optional where Wrapped == String {
/// returns nil for an empty string
var nilIfEmpty: String? {
guard let strongSelf = self else {
return nil
}
return strongSelf.isEmpty ? nil : strongSelf
}
}
51 changes: 51 additions & 0 deletions Sources/LibStringy/StringProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation

public extension StringProtocol {
/// Sparate a string by word boundaries.
///
/// - Returns: An array of strings, representing a list of words.
func words() -> [String] {
return self.substrings(options: .byWords)
}

/// Sparate a string by sentences.
///
/// - Returns: An array of strings, representing a list of sentences.
func sentences() -> [String] {
return self.substrings(options: .bySentences)
}

/// Sparate a string by paragraphs.
///
/// - Returns: An array of strings, representing a list of paragraphs.
func paragraphs() -> [String] {
return self.substrings(options: .byParagraphs)
}

/// Sparate a string by lines.
///
/// - Returns: An array of strings, representing a list of lines.
func lines() -> [String] {
return self.substrings(options: .byLines)
}

/// Sparate a string eumeration options.
///
/// - Parameter options: Options specifying types of substrings and enumeration styles.
/// If opts is omitted or empty, body is called a single time with the range of
/// the string specified by range.
/// - Returns: Array of strings, separated by the specified options.
func substrings(options: NSString.EnumerationOptions) -> [String] {
let range: Range<String.Index> = self.startIndex ..< self.endIndex
var substrings = [String]()
self.enumerateSubstrings(in: range, options: options) {substr,_,_,_ in
guard let substring = substr, !substring.isEmpty else { return }
substrings.append(substring)
}
return substrings
}

var isCapitalized: Bool {
return self.first?.isUppercase ?? false
}
}
19 changes: 19 additions & 0 deletions Sources/stringy/Subcommands/CamelCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation
import ArgumentParser
import LibStringy

extension Stringy {
struct CamelCase: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "camelcase",
abstract: "Converts strings to camelCase",
discussion: "E.G. Earth Sun Moon -> earthSunMoon"
)

@OptionGroup var options: Stringy.Options

mutating func run() {
print(options.invert ? options.string.uncamelcased() : options.string.camelcased())
}
}
}
17 changes: 17 additions & 0 deletions Sources/stringy/Subcommands/KebabCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation
import ArgumentParser
import LibStringy

struct KebabCase: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "kebabcase",
abstract: "Converts strings to kebab-case.",
discussion: "E.G. 'Words on a stick' -> 'words-on-a-stick'"
)

@OptionGroup var options: Stringy.Options

mutating func run() {
print(options.invert ? options.string.unkebabcased() : options.string.kebabcased())
}
}
17 changes: 17 additions & 0 deletions Sources/stringy/Subcommands/SnakeCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation
import ArgumentParser
import LibStringy

struct SnakeCase: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "snakecase",
abstract: "Converts strings to snake_case.",
discussion: "E.G. 'Snakes are slithery' -> 'snakes_are_slithery'"
)

@OptionGroup var options: Stringy.Options

mutating func run() {
print(options.invert ? options.string.unsnakecased() : options.string.snakecased())
}
}
Loading

0 comments on commit c02083f

Please sign in to comment.