Skip to content

Instantly share code, notes, and snippets.

@JoshuaSullivan
Created March 30, 2020 22:21
Show Gist options
  • Save JoshuaSullivan/e5fe8cd6c77fa17988237577e58a27cc to your computer and use it in GitHub Desktop.
Save JoshuaSullivan/e5fe8cd6c77fa17988237577e58a27cc to your computer and use it in GitHub Desktop.

Revisions

  1. JoshuaSullivan created this gist Mar 30, 2020.
    137 changes: 137 additions & 0 deletions synonym-search.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,137 @@
    import UIKit
    import NaturalLanguage
    import PlaygroundSupport

    //: Your secret API key from `https://dictionaryapi.com` goes here.
    //: THIS WON'T WORK UNLESS YOU GET A KEY.
    let thesaurusKey = ""

    //: The string you want to work on.
    var testString = "The bright sun set behind the green hills. Thin clouds streaked the red sky."

    //: Since we are making an API request as part of this playground, we need to allow it time to finish.
    PlaygroundPage.current.needsIndefiniteExecution = true

    /// The API returns an array of this data type. We're going to ignore everything except the synonyms.
    struct ThesaurusResponse: Decodable {
    struct Metadata: Decodable {
    /// An array of synonym arrays. Each array is related to a different homograph of the query. Picking the right
    /// one by context will require a deeper analysis of the API response.
    let synonyms: [[String]]

    enum CodingKeys: String, CodingKey {
    case synonyms = "syns"
    }
    }

    /// The top-level wrapper for metadata.
    let meta: Metadata
    }

    /// Keep track of where in the string the word is we're going to replace so that we can do efficient string replacement.
    /// Also, this will prevent all homographs identical to the query from being replaced simultaneously.
    struct ReplacementTarget {
    let value: String
    let range: Range<String.Index>
    }

    /// Create a URLRequest for the dictionary API.
    ///
    /// - Parameter query: The string which we are requesting synonyms for.
    /// - Returns: A URLRequest that can be executed to query the API.
    ///
    func createRequest(for query: String) -> URLRequest {
    guard let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
    fatalError("The query '\(query)' could not be requested.")
    }
    var urlComps = URLComponents(string: "https://www.dictionaryapi.com/api/v3/references/thesaurus/json/\(escapedQuery)")!
    urlComps.queryItems = [URLQueryItem(name: "key", value: thesaurusKey)]
    var urlRequest = URLRequest(url: urlComps.url!)
    urlRequest.httpMethod = "GET"
    return urlRequest
    }

    /// Requet sender using default configuration.
    let session = URLSession(configuration: .default)

    /// Response JSON parser.
    let decoder = JSONDecoder()

    print("Tagging the string: \(testString)")

    //: We want to examine the entire string.
    let stringRange = testString.startIndex..<testString.endIndex

    //: Create and set up the NLTagger instance that will categorize the words. We are only interested in the words,
    //: not the punctuation or white spaces.
    let tagger = NLTagger(tagSchemes: [.nameTypeOrLexicalClass])
    tagger.string = testString
    let allTags = tagger.tags(
    in: stringRange,
    unit: .word,
    scheme: .nameTypeOrLexicalClass,
    options: [.omitWhitespace, .omitPunctuation]
    )

    //: Filter the tags to find only the adjectives.
    let adjectives = allTags.compactMap { (tag, range) -> ReplacementTarget? in
    guard
    let tag = tag,
    tag == .adjective
    else { return nil }
    return ReplacementTarget(value: String(testString[range]), range: range)
    }

    print("Found \(adjectives.count) adjective(s): \(adjectives.map{ $0.value }.joined(separator: ", "))")

    //: Pick a random adjective to send to the API for synonyms.
    guard let selectedTarget = adjectives.randomElement() else {
    fatalError("No adjectives found in string.")
    }

    //: Construct and send the URLRequest.
    let query = selectedTarget.value
    print("Executing API request for '\(query)'.")
    let request = createRequest(for: query)
    let task = session.dataTask(with: request) { (data, response, error) in
    //: Whether the response is a success or failure, we want playground execution to finish once we're done here.
    defer { PlaygroundPage.current.finishExecution() }

    //: Ensure we received a response and not an error.
    guard let data = data else {
    if let error = error {
    print("ERROR: \(error.localizedDescription)")
    } else {
    print("ERROR: Query failed for an unknown reason.")
    }
    return
    }

    //: Attempt to decode the response.
    do {
    let response = try decoder.decode([ThesaurusResponse].self, from: data)

    guard
    let synonyms = response.first?.meta.synonyms.first,
    !synonyms.isEmpty
    else {
    print("No synonyms returned from the API.")
    return
    }
    //: Until such time as we do extra work to at least ensure that we are picking the correct part-of-speech
    //: ("light" as a verb or a noun, for instance), we're just picking the first synonym set and picking a
    //: random replacement.
    print("Found \(synonyms.count) synonyms: \(synonyms.joined(separator: ", "))")
    guard let substitution = synonyms.randomElement() else {
    fatalError("Unable to pick a substitution.")
    }
    print("Picked '\(substitution)' as a substitution.")
    let modifiedString = testString.replacingCharacters(in: selectedTarget.range, with: substitution)
    print("Modified string: \(modifiedString)")
    } catch {
    print("Failed to decode the API response: \(error.localizedDescription)")
    print("\(String(data: data, encoding: .utf8)!)")
    }
    }
    //: Send the request to the API.
    task.resume()