Skip to content

Instantly share code, notes, and snippets.

@jakehawken
Last active June 23, 2023 15:53
Show Gist options
  • Save jakehawken/ddfdd2fd66f91f27f3a5e7194df79ded to your computer and use it in GitHub Desktop.
Save jakehawken/ddfdd2fd66f91f27f3a5e7194df79ded to your computer and use it in GitHub Desktop.
Some handy tools for making things into more human-readable strings. Helpful for debugging!
import Foundation
protocol PrettyPrintable {
func prettyPrinted() -> String
func prettyPrintToConsole()
}
extension PrettyPrintable {
func prettyPrintToConsole() {
let pretty = prettyPrinted()
print("\n\(pretty)\n")
}
}
//MARK: - Common type conformances to PrettyPrintable
extension String: PrettyPrintable {
func prettyPrinted() -> String {
let truncated = self.truncateIfNeeded(maxLength: 40, trailing: "...")
return "\"\(truncated)\""
}
}
extension Array: PrettyPrintable {
func prettyPrinted() -> String {
var outputString = "["
forEach {
outputString += "\n"
if let prettyPrintable = $0 as? PrettyPrintable {
outputString += "\(prettyPrintable.prettyPrinted())"
}
else if let dict = $0 as? [String: Any] {
outputString += dict.prettyPrinted()
}
else if let encodable = $0 as? Encodable {
outputString += encodable.asDictionary()?.prettyPrinted() ?? "\(encodable)"
}
else {
outputString += "\($0)"
}
outputString += ","
}
outputString -= ","
outputString = outputString.replacingOccurrences(of: "\n", with: "\n ")
outputString += "\n]"
return outputString
}
}
extension Dictionary: PrettyPrintable where Key==String {
func prettyPrinted() -> String {
var outputString = "{"
for key in keys {
let value = self[key]
var valueString: String
if let prettyPrintable = value as? PrettyPrintable {
valueString = prettyPrintable.prettyPrinted()
}
else if let arrayValue = value as? [Any] {
valueString = arrayValue.prettyPrinted()
}
else if let stringVal = value as? String {
valueString = stringVal.prettyPrinted()
}
else {
valueString = (value == nil) ? "nil" : "\(value!)"
let components = valueString.components(separatedBy: "\n")
if components.count == 1 {
valueString = valueString.truncateIfNeeded(maxLength: 40, trailing: "...")
}
else {
let truncatedComponents = components.map {
$0.truncateIfNeeded(maxLength: 40, trailing: "...")
}
valueString = truncatedComponents.joined(separator: "\n")
}
}
outputString += "\n\(key) : \(valueString),"
}
outputString -= ","
// This ensures that the correct indentation is added at all recursive levels.
outputString = outputString.replacingOccurrences(of: "\n", with: "\n ")
outputString += "\n}"
return outputString
}
}
//MARK: - Quick translation of Encodable objects to Dictionaries
extension Encodable {
func asDictionary() -> [String: Any]? {
let jsonEncoder = JSONEncoder()
guard let data = try? jsonEncoder.encode(self) else {
return nil
}
guard let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) else {
return nil
}
return json as? [String: Any]
}
}
//MARK: - Class for pretty-printing anything.
class PrettyMaker {
class func makePrettyIfPossible(_ thing: Any) -> String {
var prettyString: String?
if let arrayOfAny = thing as? [Any] {
prettyString = arrayOfAny.prettyPrinted()
}
else if let encodableThing = thing as? Encodable {
prettyString = encodableThing.asDictionary()?.prettyPrinted()
}
else if let prettyPrintable = thing as? PrettyPrintable {
return prettyPrintable.prettyPrinted()
}
else {
let whatIsIt = String(describing: type(of: self))
print("Can't pretty print for type: \(whatIsIt)")
}
return prettyString ?? "\(thing)"
}
}
//MARK: - some syntactic sugar for string manipulation
fileprivate extension String {
fileprivate static func -=(lhs: inout String, rhs: String) {
guard lhs.hasSuffix(rhs) else {
return
}
guard let range = lhs.range(of: rhs, options: [.backwards], range: nil, locale: nil) else {
return
}
lhs = lhs.replacingCharacters(in: range, with: "")
}
fileprivate func truncateIfNeeded(maxLength: Int, trailing: String = "") -> String {
if self.count > maxLength {
return truncate(maxLength: maxLength)! + trailing
} else {
return self
}
}
fileprivate func truncate(maxLength: Int, encoding: String.Encoding? = nil) -> String? {
guard maxLength >= 0 else {
return nil
}
let encoding = encoding ?? self.smallestEncoding
var bytes = [UInt8](repeating: 0, count: maxLength)
var remaining = self.startIndex..<self.endIndex
var usedLength = 0
_ = self.getBytes(&bytes, maxLength: maxLength,
usedLength: &usedLength,
encoding: encoding,
options: .externalRepresentation,
range: self.startIndex..<self.endIndex, remaining: &remaining)
return String(bytes: bytes, encoding: encoding)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment