Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Flesch-Kincaid tests and syllable counting #9

Merged
merged 12 commits into from
Dec 3, 2016
56 changes: 44 additions & 12 deletions Analysis/Classes/Analysis.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
//
// Analysis.swift
//
//
// Created by Bas Broek on 11/11/2016.
//
//

import Foundation

/// The option to when calculating average length. This is either `.word` or `.sentence`.
/// The option to use when calculating average length. This is either `.word` or `.sentence`.
public enum LengthOption {
case word
case sentence
Expand All @@ -17,6 +9,7 @@ public enum LengthOption {
/// An analysis of a `String`.
public struct Analysis {
public typealias Percentage = Double
public typealias Grade = Double

/// The string used to construct the `Analysis`.
public let input: String
Expand Down Expand Up @@ -63,6 +56,13 @@ public struct Analysis {
}
}

/// Returns the total amount of syllables of the `input`.
public func syllableCount() -> Int {
return words
.map { $0.syllables }
.reduce(0, +)
}

/// Returns the character count of the `input`.
///
/// - Parameter includingSpaces: Indicating if characters
Expand All @@ -87,7 +87,7 @@ public struct Analysis {
private func _characterOccurences(caseSensitive: Bool = false) -> [Character: Int] {
var occurrences: [Character: Int] = [:]
characters
.map { (caseSensitive) ? $0 : Character(String(describing: $0).lowercased()) }
.map { (caseSensitive) ? $0 : $0.lowercased() }
.forEach { occurrences[$0] = (occurrences[$0] ?? 0) + 1 }
return occurrences
}
Expand Down Expand Up @@ -132,12 +132,22 @@ public struct Analysis {
/// should be counted regardless of their case sensitivity.
/// Defaults to `false`.
public func occurrences(of character: Character, caseSensitive: Bool = false) -> Int {
let character = (caseSensitive) ? character : Character(String(describing: character).lowercased())
let character = (caseSensitive) ? character : character.lowercased()
return characters
.map { (caseSensitive) ? $0 : Character(String(describing: $0).lowercased()) }
.map { (caseSensitive) ? $0 : $0.lowercased() }
.filter { $0 == character }.count
}

/// Returns the syllables of every unique word.
public func wordSyllables() -> [String: Int] {
var syllables: [String: Int] = [:]
let uniqueWords = Array(_wordOccurrences(caseSensitive: false).keys)

uniqueWords.forEach { syllables[$0] = $0.syllables }

return syllables
}

/// Returns the frequency of the specified word.
///
/// - Parameter caseSensitive: Indicating if words
Expand Down Expand Up @@ -184,6 +194,28 @@ public struct Analysis {
public var averageWordsPerSentence: Double {
return Double(wordCount()) / Double(sentenceCount())
}

private var _wordsPerSentences: Double {
return Double(wordCount()) / Double(sentenceCount())
}

private var _syllablesPerWords: Double {
return Double(syllableCount()) / Double(wordCount())
}

/// Returns the Flesch reading ease score.
///
/// - Note: https://en.wikipedia.org/wiki/Flesch–Kincaid_readability_tests#Flesch_reading_ease
public func fleschReadingEase() -> Percentage {
return 206.835 - 1.015 * _wordsPerSentences - 84.6 * _syllablesPerWords
}

/// Returns the Flesch-Kincaid grade level.
///
/// - Note: https://en.wikipedia.org/wiki/Flesch–Kincaid_readability_tests#Flesch.E2.80.93Kincaid_grade_level
public func fleschKincaidGradeLevel() -> Grade {
return 0.39 * _wordsPerSentences + 11.8 * _syllablesPerWords - 15.59
}
}

extension Analysis: Hashable {
Expand Down
12 changes: 12 additions & 0 deletions Analysis/Classes/Character+Casing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Foundation

internal extension Character {

func lowercased() -> Character {
return Character(String(describing: self).lowercased())
}

func uppercased() -> Character {
return Character(String(describing: self).uppercased())
}
}
8 changes: 0 additions & 8 deletions Analysis/Classes/Dictionary+Sorting.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
//
// Dictionary+Sorting.swift
//
//
// Created by Bas Broek on 11/11/2016.
//
//

import Foundation

/// The sort option of the dictionary. This is either `.key` or `.value`.
Expand Down
8 changes: 0 additions & 8 deletions Analysis/Classes/String+Analysis.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
//
// String+Analysis.swift
//
//
// Created by Bas Broek on 11/11/2016.
//
//

import Foundation

public extension String {
Expand Down
203 changes: 203 additions & 0 deletions Analysis/Classes/SyllableCounter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//
// SyllableCounter.swift
//
// Created by Warren Freitag on 2/14/16.
// Copyright © 2016 Warren Freitag. All rights reserved.
// Licensed under the Apache 2.0 License.
//
// Adapted from a Java implementation created by Hugo "m09" Mougard.
// https://github.com/m09/syllable-counter
//

import Foundation

public class SyllableCounter {

// MARK: - Shared instance

public static let shared = SyllableCounter()

// MARK: - Private properties

private var exceptions: [String: Int] = [
"brutes": 1,
"chummed": 1,
"flapped": 1,
"foamed": 1,
"gaped": 1,
"h'm": 1,
"lb": 1,
"mimes": 1,
"ms": 1,
"peeped": 1,
"sheered": 1,
"st": 1,
"queue": 1,
"none": 1,
"leaves": 1,
"awesome": 2,
"60": 2,
"capered": 2,
"caressed": 2,
"clattered": 2,
"deafened": 2,
"dr": 2,
"effaced": 2,
"effaces": 2,
"fringed": 2,
"greyish": 2,
"jr": 2,
"mangroves": 2,
"messieurs": 2,
"motioned": 2,
"moustaches": 2,
"mr": 2,
"mrs": 2,
"pencilled": 2,
"poleman": 2,
"quivered": 2,
"reclined": 2,
"shivered": 2,
"sidespring": 2,
"slandered": 2,
"sombre": 2,
"sr": 2,
"stammered": 2,
"suavely": 2,
"tottered": 2,
"trespassed": 2,
"truckle": 2,
"unstained": 2,
"therefore": 2,
"businesses": 3,
"bottleful": 3,
"discoloured": 3,
"disinterred": 3,
"hemispheres": 3,
"manoeuvred": 3,
"sepulchre": 3,
"shamefully": 3,
"unexpressed": 3,
"veriest": 3,
"wyoming": 3,
"etc": 4,
"sailmaker": 4,
"satiated": 4,
"sententiously": 4,
"particularized": 5,
"unostentatious": 5,
"propitiatory": 6,
]

private var addSyllables: [NSRegularExpression]!
private var subSyllables: [NSRegularExpression]!

private let vowels: Set<Character> = ["a", "e", "i", "o", "u", "y"]

// MARK: - Error enum

private enum SyllableCounterError: Error {
case badRegex(String)
case badExceptionsData(String)
}

// MARK: - Constructors

public init() {
do {
try populateAddSyllables()
try populateSubSyllables()
}
catch SyllableCounterError.badRegex(let pattern) {
print("Bad Regex pattern: \(pattern)")
}
catch SyllableCounterError.badExceptionsData(let info) {
print("Problem parsing exceptions dataset: \(info)")
}
catch {
print("An unexpected error occured while initializing the syllable counter.")
}
}

// MARK: - Setup

private func populateAddSyllables() throws {
try addSyllables = buildRegexes(forPatterns: [
"ia", "riet", "dien", "iu", "io", "ii",
"[aeiouy]bl$", "mbl$", "tl$", "sl$", "[aeiou]{3}",
"^mc", "ism$", "(.)(?!\\1)([aeiouy])\\2l$", "[^l]llien", "^coad.",
"^coag.", "^coal.", "^coax.", "(.)(?!\\1)[gq]ua(.)(?!\\2)[aeiou]", "dnt$",
"thm$", "ier$", "iest$", "[^aeiou][aeiouy]ing$"])
}

private func populateSubSyllables() throws {
try subSyllables = buildRegexes(forPatterns: [
"cial", "cian", "tia", "cius", "cious",
"gui", "ion", "iou", "sia$", ".ely$",
"ves$", "geous$", "gious$", "[^aeiou]eful$", ".red$"])
}

private func buildRegexes(forPatterns patterns: [String]) throws -> [NSRegularExpression] {
return try patterns.map { pattern -> NSRegularExpression in
do {
let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines])
return regex
}
catch {
throw SyllableCounterError.badRegex(pattern)
}
}
}

// MARK: - Public methods

internal func count(word: String) -> Int {
if word.characters.count <= 1 {
return word.characters.count
}

var mutatedWord = word.lowercased(with: Locale(identifier: "en_US")).trimmingCharacters(in: .punctuationCharacters)

if let exceptionValue = exceptions[mutatedWord] {
return exceptionValue
}

if mutatedWord.characters.last == "e" {
mutatedWord = String(mutatedWord.characters.dropLast())
}

var count = 0
var previousIsVowel = false

for character in mutatedWord.characters {
let isVowel = vowels.contains(character)
if isVowel && !previousIsVowel {
count += 1
}
previousIsVowel = isVowel
}

for pattern in addSyllables {
let matches = pattern.matches(in: mutatedWord, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSRange(location: 0, length: mutatedWord.characters.count))
if !matches.isEmpty {
count += 1
}
}

for pattern in subSyllables {
let matches = pattern.matches(in: mutatedWord, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSRange(location: 0, length: mutatedWord.characters.count))
if !matches.isEmpty {
count -= 1
}
}

return (count > 0) ? count : 1
}
}

extension String {

internal var syllables: Int {
return SyllableCounter.shared.count(word: self)
}
}
9 changes: 9 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# next

- Added `syllableCount()`, which counts the total amount of syllables of the `input`.
- Added `wordSyllables()`, which returns the syllables of every unique word.
- Added `fleschReadingEase()`, which calculates the [Flesch reading ease score](https://en.wikipedia.org/wiki/Flesch–Kincaid_readability_tests#Flesch_reading_ease).
- Added `fleschKincaidGradeLevel()`, which calculates the [Flesch-Kincaid grade level](https://en.wikipedia.org/wiki/Flesch–Kincaid_readability_tests#Flesch.E2.80.93Kincaid_grade_level).

# [0.1.0](https://github.com/BasThomas/Analysis/releases/tag/0.1.0)
Initial release.
4 changes: 4 additions & 0 deletions Example/Analysis.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
4978479C1DD87E4A003CFFBB /* Analysis.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4978479B1DD87E4A003CFFBB /* Analysis.storyboard */; };
4978479E1DD88148003CFFBB /* AnalysisTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4978479D1DD88148003CFFBB /* AnalysisTableViewController.swift */; };
4985BEC11DF1959500B36F51 /* UIViewController+Safari.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4985BEC01DF1959500B36F51 /* UIViewController+Safari.swift */; };
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
Expand Down Expand Up @@ -36,6 +37,7 @@
45E49C4F9720F93380BF3A10 /* Pods-Analysis_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Analysis_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Analysis_Tests/Pods-Analysis_Tests.debug.xcconfig"; sourceTree = "<group>"; };
4978479B1DD87E4A003CFFBB /* Analysis.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Analysis.storyboard; sourceTree = "<group>"; };
4978479D1DD88148003CFFBB /* AnalysisTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalysisTableViewController.swift; sourceTree = "<group>"; };
4985BEC01DF1959500B36F51 /* UIViewController+Safari.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Safari.swift"; sourceTree = "<group>"; };
607FACD01AFB9204008FA782 /* Analysis_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Analysis_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -111,6 +113,7 @@
607FACD51AFB9204008FA782 /* AppDelegate.swift */,
607FACD71AFB9204008FA782 /* ViewController.swift */,
4978479D1DD88148003CFFBB /* AnalysisTableViewController.swift */,
4985BEC01DF1959500B36F51 /* UIViewController+Safari.swift */,
607FACD91AFB9204008FA782 /* Main.storyboard */,
4978479B1DD87E4A003CFFBB /* Analysis.storyboard */,
607FACDC1AFB9204008FA782 /* Images.xcassets */,
Expand Down Expand Up @@ -370,6 +373,7 @@
files = (
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
4985BEC11DF1959500B36F51 /* UIViewController+Safari.swift in Sources */,
4978479E1DD88148003CFFBB /* AnalysisTableViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Loading