{"id":133,"date":"2020-02-20T01:35:02","date_gmt":"2020-02-20T01:35:02","guid":{"rendered":"https:\/\/www.aya.io\/ayablog\/?p=133"},"modified":"2022-02-07T01:41:21","modified_gmt":"2022-02-07T01:41:21","slug":"pnutsays","status":"publish","type":"post","link":"https:\/\/www.aya.io\/blog\/pnutsays\/","title":{"rendered":"PnutSays &#8211; cowsay pour Pnut en Swift"},"content":{"rendered":"<p>Lors d'un hackathon sur Pnut.io j'avais tent\u00e9 de code un wrapper en Bash pour <a href=\"https:\/\/fr.wikipedia.org\/wiki\/Cowsay\">cowsay<\/a> pour le rendre compatible avec les posts destin\u00e9s \u00e0 la mini imprimante communautaire.<\/p>\n<p>Le r\u00e9sultat \u00e9tait bancale, je me suis donc ensuite d\u00e9cider \u00e0 faire une version en Swift \u00e0 partir de z\u00e9ro.<\/p>\n<p><!-- more --><\/p>\n<p><img decoding=\"async\" src=\"http:\/\/aya.io\/misc\/pnutsays-welcome.png\" alt=\"usage\" \/><\/p>\n<p>On commence par faire un <code>main<\/code> pour parse les commandes avec ProcessInfo et ArgumentParser :<\/p>\n<pre><code class=\"language-swift\">import SPMUtility\nimport Foundation\n\nlet arguments = ProcessInfo.processInfo.arguments.dropFirst()\nlet parser = ArgumentParser(usage: &quot;&lt;options&gt; \\&quot;some text\\&quot;&quot;, overview: &quot;A command-line tool similar to cowsay, dedicated to pnut.io&quot;)\nlet messageArgument = parser.add(positional: &quot;\\&quot;some text\\&quot;&quot;, kind: String.self, optional: false, usage: &quot;The text that will be included in the speech bubble&quot;)\nlet pnutprinterArgument = parser.add(option: &quot;--pnutprinter&quot;, shortName: &quot;-p&quot;, kind: Bool.self, usage: &quot;Adds a tag which triggers pnut&#039;s printer&quot;)\nlet errrorMessage = &quot;You need to pass \\&quot;some text\\&quot; to generate the message. Use --help to list arguments and options.&quot;\n\ndo {\n    let parsedArguments = try parser.parse(Array(arguments))\n    if let msg = parsedArguments.get(messageArgument) {\n        let ps = PnutSays(thoughts: msg)\n        if let _ = parsedArguments.get(pnutprinterArgument) {\n            print(ps.say(pnutPrinter: true))\n        } else {\n            print(ps.say())\n        }\n    } else {\n        print(errrorMessage)\n    }\n} catch {\n    print(&quot;An error occured!\\n`\\(error)`.\\n\\(errrorMessage)&quot;)\n}<\/code><\/pre>\n<p>Ensuite, la principale difficult\u00e9 c'est d'adapter automatiquement la largeur de la bulle, ainsi que les angles, pour la bordure comme pour les retours \u00e0 la ligne.<\/p>\n<p>La manipulation des Strings avec Swift c'est pas la f\u00eate mais en d\u00e9composant bien ce que l'on veut achever en plus petites t\u00e2ches on finit par rompre la complexit\u00e9.<\/p>\n<p>Donc d\u00e9j\u00e0, une extension pour nous aider :<\/p>\n<pre><code class=\"language-swift\">extension String {\n\n    func splitByLength(_ length: Int, seperator: String) -&gt; [String] {\n        var result = [String]()\n        var collectedWords = [String]()\n        collectedWords.reserveCapacity(length)\n        var count = 0\n        let words = self.split { $0 == &quot; &quot; }.map(String.init)\n        for word in words {\n            count += word.count + 1 \/\/add 1 to include space\n            if (count &gt; length) {\n                result.append(collectedWords.map { String($0) }.joined(separator: seperator) )\n                collectedWords.removeAll(keepingCapacity: true)\n                count = word.count\n                collectedWords.append(word)\n            } else {\n                collectedWords.append(word)\n            }\n        }\n        if !collectedWords.isEmpty {\n            result.append(collectedWords.map { String($0) }.joined(separator: seperator))\n        }\n        return result\n    }\n\n    func components(withMaxLength length: Int) -&gt; [String] {\n        return stride(from: 0, to: self.count, by: length).map {\n            let start = self.index(self.startIndex, offsetBy: $0)\n            let end = self.index(start, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex\n            return String(self[start..&lt;end])\n        }\n    }\n\n    func removingLeadingSpaces() -&gt; String {\n        guard let index = self.firstIndex(where: { String($0) != &quot; &quot; }) else {\n            return self\n        }\n        return String(self[index...])\n    }\n}<\/code><\/pre>\n<p>Et voici la classe principale :<\/p>\n<pre><code class=\"language-swift\">class PnutSays {\n\n    private let thoughts: String\n    private let maxWordLength: Int\n\n    private lazy var wrapped = computeWrapped()\n    private func computeWrapped() -&gt; [String] {\n        let firstPass = thoughts.split { $0.isNewline }\n        var result = [String]()\n        for block in firstPass {\n            let cut = String(block).splitByLength(maxWordLength, seperator: &quot; &quot;)\n            for subBlock in cut {\n                let components = subBlock.components(withMaxLength: maxWordLength)\n                for element in components {\n                    result.append(element.removingLeadingSpaces())\n                }\n            }\n        }\n        if result.isEmpty {\n            result.append(&quot;???&quot;)\n        }\n        return result\n    }\n\n    private lazy var longestLine = computeLongestLine()\n    private func computeLongestLine() -&gt; Int {\n        let m = wrapped.max { (a, b) -&gt; Bool in\n            return a.count &lt; b.count\n        }\n        return m!.count\n    }\n\n    private func makePadding(with s: String) -&gt; String {\n        let toAdd = longestLine - s.count\n        var base = s\n        for _ in 0 ... toAdd {\n            base.append(&quot; &quot;)\n        }\n        return base\n    }\n\n    private lazy var horizontalSeparator = makeHorizontalSeparator()\n    private func makeHorizontalSeparator() -&gt; String {\n        var base = &quot; &quot;\n        for _ in 0 ... longestLine + 1 {\n            base.append(&quot;\u2013&quot;)\n        }\n        base.append(&quot; &quot;)\n        return base\n    }\n\n    init(thoughts: String) {\n        self.thoughts = thoughts\n        self.maxWordLength = 15 \/\/ 15 is nice length for pnut cow\n    }\n\n    private let firstSeparatorStart = &quot;\/&quot;\n    private let firstSeparatorEnd = &quot;\\\\&quot;\n    private let lastSeparatorStart = &quot;\\\\&quot;\n    private let lastSeparatorEnd = &quot;\/&quot;\n    private let middleSeparator = &quot;|&quot;\n\n    private let cow =\n    &quot;&quot;&quot;\n     \\\\   ,__,\n      \\\\  (oo)____\n         (__)    )\\\\\n            ||--|| *\n    &quot;&quot;&quot;\n\n    func say(pnutPrinter: Bool = false) -&gt; String {\n        let num = wrapped.count\n        if num == 1 {\n            return makeOneLine(pnutPrinter: pnutPrinter)\n        } else if num == 2 {\n            return makeTwoLines(pnutPrinter: pnutPrinter)\n        } else {\n            return makeMultipleLines(number: num, pnutPrinter: pnutPrinter)\n        }\n    }\n\n    private func makeOneLine(pnutPrinter: Bool) -&gt; String {\n        var base = makeFirstLine(with: wrapped[0], unique: true)\n        base.append(horizontalSeparator + &quot;\\n&quot; + cow)\n        if pnutPrinter {\n            base.append(&quot;\\n\\n&quot; + &quot;#pnutprinter*&quot;)\n        }\n        return base\n    }\n\n    private func makeTwoLines(pnutPrinter: Bool) -&gt; String {\n        let base = makeFirstLine(with: wrapped[0], unique: false)\n        var last = makeLastLine(with: base, phrase: wrapped[1])\n        if pnutPrinter {\n            last.append(&quot;\\n\\n&quot; + &quot;#pnutprinter*&quot;)\n        }\n        return last\n    }\n\n    private func makeMultipleLines(number: Int, pnutPrinter: Bool) -&gt; String {\n        var base = makeFirstLine(with: wrapped[0], unique: false)\n        for i in 1 ..&lt; number - 1 {\n            base.append(middleSeparator + &quot; &quot; + makePadding(with: wrapped[i]) + middleSeparator + &quot;\\n&quot;)\n        }\n        var last = makeLastLine(with: base, phrase: wrapped[number - 1])\n        if pnutPrinter {\n            last.append(&quot;\\n\\n&quot; + &quot;#pnutprinter*&quot;)\n        }\n        return last\n    }\n\n    private func makeFirstLine(with phrase: String, unique: Bool) -&gt; String {\n        var base = horizontalSeparator\n        if unique {\n            base.append(&quot;\\n&quot; + middleSeparator + &quot; &quot; + makePadding(with: phrase) + middleSeparator + &quot;\\n&quot;)\n        } else {\n            base.append(&quot;\\n&quot; + firstSeparatorStart + &quot; &quot; + makePadding(with: phrase) + firstSeparatorEnd + &quot;\\n&quot;)\n        }\n        return base\n    }\n\n    private func makeLastLine(with base: String, phrase: String) -&gt; String {\n        var base = base\n        base.append(lastSeparatorStart + &quot; &quot; + makePadding(with: phrase) + lastSeparatorEnd + &quot;\\n&quot;)\n        base.append(horizontalSeparator + &quot;\\n&quot; + cow)\n        return base\n    }\n}<\/code><\/pre>\n<p>Pour le compiler, il vous faudra d'abord <a href=\"https:\/\/swift.org\/download\/\">Swift<\/a>, suivez <a href=\"https:\/\/swift.org\/download\/#using-downloads\">leur guide<\/a> au besoin.<\/p>\n<p>Ensuite, <code>cd<\/code> dans le dossier puis<\/p>\n<pre><code class=\"language-bash\">swift build<\/code><\/pre>\n<p>Il n'y a plus qu'\u00e0 le copier et le rendre ex\u00e9cutable :<\/p>\n<pre><code class=\"language-bash\">cp -f .build\/release\/pnutsays \/usr\/local\/bin\/pnutsays<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Lors d&rsquo;un hackathon sur Pnut.io j&rsquo;avais tent\u00e9 de code un wrapper en Bash pour cowsay pour le rendre compatible avec les posts destin\u00e9s \u00e0 la mini imprimante communautaire. Le r\u00e9sultat \u00e9tait bancale, je me suis donc ensuite d\u00e9cider \u00e0 faire une version en Swift \u00e0 partir de z\u00e9ro. On commence par faire un main pour&hellip; <a class=\"more-link\" href=\"https:\/\/www.aya.io\/blog\/pnutsays\/\">Poursuivre la lecture <span class=\"screen-reader-text\">PnutSays &#8211; cowsay pour Pnut en Swift<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":138,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5,6,11],"tags":[],"class_list":["post-133","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dev","category-swift","category-term","entry"],"_links":{"self":[{"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/posts\/133","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/comments?post=133"}],"version-history":[{"count":8,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/posts\/133\/revisions"}],"predecessor-version":[{"id":142,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/posts\/133\/revisions\/142"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/media\/138"}],"wp:attachment":[{"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/media?parent=133"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/categories?post=133"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/tags?post=133"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}