├── .github ├── FUNDING.yml └── workflows │ ├── python-publish.yml │ └── swift.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── images ├── logo.png ├── meme.png └── social.png ├── python ├── __init__.py ├── examples │ ├── rps.py │ ├── shakespeare.py │ └── shakespeare.txt ├── src │ ├── __init__.py │ └── marc.py └── tests │ ├── __init__.py │ └── test_marc.py ├── setup.py └── swift ├── Examples ├── RPS.playground │ ├── Contents.swift │ ├── contents.xcplayground │ └── timeline.xctimeline └── Shakespeare.playground │ ├── Contents.swift │ ├── Resources │ └── shakespeare.txt │ ├── contents.xcplayground │ └── timeline.xctimeline ├── Sources └── Marc │ └── MarkovChain.swift └── Tests └── MarcTests └── MarcTests.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: maxhumber 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload to PyPI 2 | on: 3 | release: 4 | types: [published] 5 | permissions: 6 | contents: read 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v3 14 | with: 15 | python-version: '3.x' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -e . 20 | - name: Install build dependencies 21 | run: pip install build 22 | - name: Run tests 23 | run: python -m unittest 24 | - name: Build package 25 | run: python -m build 26 | - name: Publish package 27 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 28 | with: 29 | user: __token__ 30 | password: ${{ secrets.PYPI_API_TOKEN }} 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: macos-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Build 11 | run: swift build -v 12 | - name: Run tests 13 | run: swift test -v 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # custom 2 | .swiftpm 3 | playground.py 4 | .vscode 5 | .build 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Max Humber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Marc", 6 | products: [ 7 | .library(name: "Marc", targets: ["Marc"]) 8 | ], 9 | targets: [ 10 | .target(name: "Marc", path: "swift/Sources"), 11 | .testTarget(name: "MarcTests", dependencies: ["Marc"], path: "swift/Tests") 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | marc 3 |
4 | 5 | 6 | ### About 7 | 8 | marc is a **Mar**kov **c**hain generator for Python and/or Swift 9 | 10 | 11 | ### Python 12 | 13 | Install 14 | 15 | ```sh 16 | pip install marc 17 | ``` 18 | 19 | 20 | Quickstart: 21 | 22 | ```python 23 | from marc import MarkovChain 24 | 25 | player_throws = "RRRSRSRRPRPSPPRPSSSPRSPSPRRRPSSPRRPRSRPRPSSSPRPRPSSRPSRPRSSPRP" 26 | sequence = [throw for throw in player_throws] 27 | # ['R', 'R', 'R', 'S', 'R', 'S', 'R', ...] 28 | 29 | chain = MarkovChain(sequence) 30 | chain.update("R", "S") 31 | 32 | chain["R"] 33 | # {'P': 0.5, 'R': 0.25, 'S': 0.25} 34 | 35 | player_last_throw = "R" 36 | player_predicted_next_throw = chain.next(player_last_throw) 37 | # 'P' 38 | 39 | counters = {"R": "P", "P": "S", "S": "R"} 40 | counter_throw = counters[player_predicted_next_throw] 41 | # 'S' 42 | ``` 43 | 44 | For more inspiration see the [python/examples/](python/examples/) directory 45 | 46 | 47 | ### Swift 48 | 49 | SPM: 50 | 51 | ```swift 52 | dependencies: [ 53 | .package(url: "https://github.com/maxhumber/marc.git", .upToNextMajor(from: "22.5.0")) 54 | ] 55 | ``` 56 | 57 | 58 | Quickstart: 59 | 60 | ```swift 61 | import Marc 62 | 63 | let playerThrows = "RRRSRSRRPRPSPPRPSSSPRSPSPRRRPSSPRRPRSRPRPSSSPRPRPSSRPSRPRSSPRP" 64 | let sequence = playerThrows.map { String($0) } 65 | 66 | let chain = MarkovChain(sequence) 67 | chain.update("R", "S") 68 | 69 | print(chain["R"]) 70 | // [("P", 0.5), ("R", 0.25), ("S", 0.25)] 71 | 72 | let playerLastThrow = "R" 73 | let playerPredictedNextThrow = chain.next(playerLastThrow)! 74 | 75 | let counters = ["R": "P", "P": "S", "S": "R"] 76 | let counterThrow = counters[playerPredictedNextThrow]! 77 | print(counterThrow) 78 | // "S" 79 | ``` 80 | 81 | For more inspiration see the [swift/Examples/](swift/Examples/) directory 82 | 83 | 84 | ### API/Comparison 85 | 86 | | | Python | Swift | 87 | | ----------------------- | -------------------------------------- | ------------------------------------------ | 88 | | Import | `from marc import MarkovChain` | `import Marc` | 89 | | Initialize A | `chain = MarkovChain()` | `chain = MarkovChain()` | 90 | | Initialize B | `chain = MarkovChain(["R", "P", "S"])` | `let chain = MarkovChain(["R", "P", "S"])` | 91 | | Update chain | `chain.update("R", "P")` | `chain.update("R", "P")` | 92 | | Lookup transitions | `chain["R"]` | `chain["R"]` | 93 | | Generate next | `chain.next("R")` | `chain.next("R")!` | 94 | 95 | 96 | ### Why 97 | 98 | I built the first versions of *marc* in the Fall of 2019. Back then I created, and used, it as a teaching tool (for how to build and upload a PyPI package). Since March 2020 I've been spending less and less time with Python and more and more time with Swift... and so, just kind of forgot about *marc*. 99 | 100 | Recently, I had an iOS project come up that needed some Markov chains. After surveying GitHub and not finding any implementations that I liked (forgetting that I had already rolled my own in Python) I started from scratch on a new implementation in Swift. 101 | 102 | Just as I was finishing the Swift package I re-discovered *marc*... I had a good laugh looking back through the [original](https://github.com/maxhumber/marc/tree/5ea21639aba16fcfe15c5de25049d024e0bb3332) Python library. My feelings about the code I wrote and my abilities in 2019 can be summarized in a picture: 103 | 104 |
105 | meme 106 |
107 | 108 | 109 | Unable to resist a good procrasticode™ project, I cross-ported the finished Swift package to Python and polished up both codebases and documentation into this mono repo. 110 | 111 | Honestly, I had a lot of fun trying to mirror the APIs as closely as possible while doing my best to keep the Python code "Pythonic" and the Swift code "Schwifty". The whole project/exercise was incredibly rewarding, interesting, and insightful. Crudely, here's how I found working on both packages: 112 | 113 | **Python** 114 | 115 | | Like | Dislike | 116 | | ----------------------------------- | ---------------------------------------- | 117 | | `defaultdict` !! | Clunky `setup.py` packaging | 118 | | `random.choice` ! | Setting up and working with environments | 119 | | Dictionary comprehensions + sorting | `__init__.py` and directory issues | 120 | 121 | **Swift** 122 | 123 | | Like | Dislike | 124 | | ------------------------------------------------- | ---------------------------------------------- | 125 | | `Package.swift` and packaging in general | Dictionary performance sucks... (surprising!!) | 126 | | Don't have to think about environments | Need randomness? Too bad. Go roll it yourself | 127 | | `XCTest` is nicer/easier than `unittest`/`pytest` | Playgrounds aren't as good as Hydrogen/Jupyter | 128 | 129 | So why? For fun! And procrastination. And, more seriously, because I needed some chains in Swift. And then, because I thought it could be interesting to create a Rosetta Stone for Python and Swift... So if you, Dear Reader, are looking to use Markov chains in your Python or Swift project, or are looking to jump to or from either language, I hope you find this useful. 130 | 131 | 132 | ### Warning 133 | 134 | marc 22.5+ is incompatible with marc 2.x 135 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxhumber/marc/71cc93e9ae4132c3b407f09d4b361d952a66ff02/images/logo.png -------------------------------------------------------------------------------- /images/meme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxhumber/marc/71cc93e9ae4132c3b407f09d4b361d952a66ff02/images/meme.png -------------------------------------------------------------------------------- /images/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxhumber/marc/71cc93e9ae4132c3b407f09d4b361d952a66ff02/images/social.png -------------------------------------------------------------------------------- /python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxhumber/marc/71cc93e9ae4132c3b407f09d4b361d952a66ff02/python/__init__.py -------------------------------------------------------------------------------- /python/examples/rps.py: -------------------------------------------------------------------------------- 1 | from marc import MarkovChain 2 | 3 | player_throws = "RRRSRSRRPRPSPPRPSSSPRSPSPRRRPSSPRRPRSRPRPSSSPRPRPSSRPSRPRSSPRP" 4 | sequence = [throw for throw in player_throws] 5 | # ['R', 'R', 'R', 'S', 'R', 'S', 'R', ...] 6 | 7 | chain = MarkovChain(sequence) 8 | chain.update("R", "S") 9 | 10 | chain["R"] 11 | # {'P': 0.5, 'R': 0.25, 'S': 0.25} 12 | 13 | player_last_throw = "R" 14 | player_predicted_next_throw = chain.next(player_last_throw) 15 | # 'P' 16 | 17 | counters = {"R": "P", "P": "S", "S": "R"} 18 | counter_throw = counters[player_predicted_next_throw] 19 | # 'S' 20 | -------------------------------------------------------------------------------- /python/examples/shakespeare.py: -------------------------------------------------------------------------------- 1 | import random 2 | import re 3 | from marc import MarkovChain 4 | 5 | text = "" 6 | with open("python/examples/shakespeare.txt", "r") as f: 7 | for line in f.readlines(): 8 | text += line 9 | 10 | tokens = re.findall(r"[\w']+|[.,!?;]", text) 11 | 12 | chain = MarkovChain(tokens) 13 | 14 | word = random.choice(tokens) 15 | # 'Who' 16 | 17 | chain[word] 18 | # { 19 | # ',': 0.12915601023017903, 20 | # 'is': 0.08695652173913043, 21 | # 's': 0.05115089514066496, 22 | # 'hath': 0.02557544757033248, 23 | # 'was': 0.021739130434782608, 24 | # 'can': 0.020460358056265986, 25 | # 'shall': 0.01918158567774936, 26 | # 'would': 0.017902813299232736, 27 | # ... 28 | # } 29 | 30 | words = [] 31 | for i in range(25): 32 | words.append(word) 33 | word = chain.next(word) 34 | 35 | sentence = re.sub(r'\s([?.!,;_"](?:\s|$))', r"\1", " ".join(words)) 36 | # 'Who is not being sensible in men what shall I shall attend him; then. Fear you love our brother, or both friend' 37 | -------------------------------------------------------------------------------- /python/src/__init__.py: -------------------------------------------------------------------------------- 1 | from .marc import MarkovChain 2 | -------------------------------------------------------------------------------- /python/src/marc.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import random 3 | 4 | 5 | class MarkovChain: 6 | def __init__(self, sequence=None): 7 | """Initialize chain with/without starting sequence 8 | 9 | Examples: 10 | ``` 11 | chain = MarkovChain(["R", "P", "S"]) 12 | chain2 = MarkovChain() 13 | ``` 14 | """ 15 | self._store = defaultdict(lambda: defaultdict(lambda: 0)) 16 | if not sequence: 17 | return 18 | for a, b in zip(sequence, sequence[1:]): 19 | self.update(a, b) 20 | 21 | def __getitem__(self, state): 22 | """Lookup transition probabilities for state 23 | 24 | Example: 25 | ``` 26 | probs = chain["R"] 27 | ``` 28 | """ 29 | options = sorted(self._store[state].items(), key=lambda i: -i[1]) 30 | total = sum(self._store[state].values()) 31 | return {option: weight / total for (option, weight) in options} 32 | 33 | def update(self, a, b): 34 | """Update chain with transition a -> b 35 | 36 | Example: 37 | ``` 38 | chain.update("R", "P") 39 | ``` 40 | """ 41 | self._store[a][b] += 1 42 | 43 | def next(self, after): 44 | """Generate next state from chain 45 | 46 | Example: 47 | ``` 48 | chain.next("R") 49 | ``` 50 | """ 51 | options = list(self._store[after].keys()) 52 | weights = self._store[after].values() 53 | return random.choices(options, weights, k=1)[0] 54 | -------------------------------------------------------------------------------- /python/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxhumber/marc/71cc93e9ae4132c3b407f09d4b361d952a66ff02/python/tests/__init__.py -------------------------------------------------------------------------------- /python/tests/test_marc.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from marc import MarkovChain 3 | 4 | 5 | class TestMarc(TestCase): 6 | def setUp(self): 7 | player_throws = "RRRSRSRRPRPSPPRPSSSPRSPSPRRRPSSPRRPRSRPRPSSSPRPRPSSRPSRPRSSPRP" 8 | sequence = [throw for throw in player_throws] 9 | self.chain = MarkovChain(sequence) 10 | 11 | def test_lookup(self): 12 | result = self.chain["R"]["P"] 13 | self.assertAlmostEqual(result, 0.5217391304347826) 14 | 15 | def test_update(self): 16 | self.chain.update("R", "S") 17 | probs = self.chain["R"] 18 | rock = probs["R"] 19 | self.assertEqual(rock, 0.25) 20 | 21 | def test_next(self): 22 | next_state = self.chain.next("R") 23 | self.assertTrue(next_state in ["R", "P", "S"]) 24 | 25 | 26 | if __name__ == "__main__": 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("README.md", "r", encoding="utf-8") as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name="marc", 8 | version="22.5.1", 9 | url="https://github.com/maxhumber/marc", 10 | description="Markov chain generator", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | author="Max Humber", 14 | author_email="max.humber@gmail.com", 15 | license="MIT", 16 | classifiers=[ 17 | "Development Status :: 5 - Production/Stable", 18 | "License :: OSI Approved :: MIT License", 19 | "Programming Language :: Python :: 3", 20 | ], 21 | packages=[""], 22 | package_dir={"": "python/src"}, 23 | python_requires=">=3.9", 24 | setup_requires=["setuptools>=62.1.0"], 25 | ) 26 | -------------------------------------------------------------------------------- /swift/Examples/RPS.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import Marc 2 | 3 | let playerThrows = "RRRSRSRRPRPSPPRPSSSPRSPSPRRRPSSPRRPRSRPRPSSSPRPRPSSRPSRPRSSPRP" 4 | let sequence = playerThrows.map { String($0) } 5 | 6 | let chain = MarkovChain(sequence) 7 | chain.update("R", "S") 8 | 9 | print(chain["R"]) 10 | // [("P", 0.5), ("R", 0.25), ("S", 0.25)] 11 | 12 | let playerLastThrow = "R" 13 | let playerPredictedNextThrow = chain.next(playerLastThrow)! 14 | 15 | let counters = ["R": "P", "P": "S", "S": "R"] 16 | let counterThrow = counters[playerPredictedNextThrow]! 17 | print(counterThrow) 18 | // "S" 19 | -------------------------------------------------------------------------------- /swift/Examples/RPS.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /swift/Examples/RPS.playground/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /swift/Examples/Shakespeare.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NaturalLanguage 3 | import Marc 4 | 5 | let fileUrl = Bundle.main.url(forResource: "shakespeare", withExtension: "txt")! 6 | let text = try! String(contentsOf: fileUrl, encoding: .utf8) 7 | 8 | let tokenizer = NLTokenizer(unit: .word) 9 | tokenizer.string = text 10 | let tokens = tokenizer.tokens(for: text.startIndex.. 2 | 3 | 4 | -------------------------------------------------------------------------------- /swift/Examples/Shakespeare.playground/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /swift/Sources/Marc/MarkovChain.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class MarkovChain { 4 | private typealias Store = [Element: [Element: UInt]] 5 | 6 | private var store = Store() 7 | 8 | public init() {} 9 | 10 | /// Initialize chain with sequence of elements 11 | /// 12 | /// Example: 13 | /// ``` 14 | /// let chain = MarkovChain(["R", "P", "S"]) 15 | /// ``` 16 | public init(_ sequence: S) where S.Element == Element { 17 | // reducing per: https://developer.apple.com/documentation/swift/dictionary/3127177-reduce 18 | store = zip(sequence, sequence.dropFirst(1)).reduce(into: Store()) { result, pair in 19 | result[pair.0, default: [:]][pair.1, default: 0] += 1 20 | } 21 | } 22 | 23 | /// Lookup transition probabilities for state 24 | /// 25 | /// Example: 26 | /// ``` 27 | /// let transitionProbabilities = chain["R"] 28 | /// ``` 29 | public subscript(state: Element) -> [(Element, Double)] { 30 | let options = store[state, default: [:]] 31 | let total = options.values.reduce(0, +) 32 | return options 33 | .sorted { $0.value > $1.value } 34 | .map { ($0.key, Double($0.value) / Double(total)) } 35 | } 36 | 37 | /// Update chain with transition a -> b 38 | /// 39 | /// - Parameters: 40 | /// - a: Transition from state 41 | /// - b: Transition to state 42 | /// 43 | /// Example: 44 | /// ``` 45 | /// chain.update("R", "B") 46 | /// ``` 47 | public func update(_ a: Element, _ b: Element) { 48 | store[a, default: [:]][b, default: 0] += 1 49 | } 50 | 51 | /// Generate next state from chain 52 | /// 53 | /// - Parameters: 54 | /// - after: Generate following state 55 | /// 56 | /// Example: 57 | /// ``` 58 | /// let nextState = chain.next("R") 59 | /// ``` 60 | public func next(_ after: Element) -> Element? { 61 | let options = store[after, default: [:]] 62 | let total = options.values.reduce(0, +) 63 | let rand = UInt(arc4random_uniform(UInt32(total))) 64 | var sum = UInt(0) 65 | for (option, weight) in options { 66 | sum += weight 67 | if rand < sum { 68 | return option 69 | } 70 | } 71 | return nil 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /swift/Tests/MarcTests/MarcTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Marc 3 | 4 | final class MarcTests: XCTestCase { 5 | var chain: MarkovChain! 6 | 7 | typealias PerfStore = [Int: [Int: Int]] 8 | let perfSeq = (0..<10_000).map { _ in Int.random(in: 1...5) } 9 | var perfStore: PerfStore! 10 | 11 | override func setUp() { 12 | let playerThrows = "RRRSRSRRPRPSPPRPSSSPRSPSPRRRPSSPRRPRSRPRPSSSPRPRPSSRPSRPRSSPRP" 13 | let sequence = playerThrows.map { String($0) } 14 | chain = MarkovChain(sequence) 15 | perfStore = PerfStore() 16 | } 17 | 18 | func testSubscript() { 19 | let probs = chain["R"] 20 | let paperProb = probs[0].1 21 | XCTAssertEqual(paperProb, 0.5217391304347826) 22 | } 23 | 24 | func testUpdate() { 25 | chain.update("R", "S") 26 | let probs = chain["R"] 27 | let rockProb = probs[2].1 28 | XCTAssertEqual(rockProb, 0.25) 29 | } 30 | 31 | func testNext() { 32 | let next = chain.next("R")! 33 | let contains = ["R", "P", "S"].contains(next) 34 | XCTAssertTrue(contains) 35 | } 36 | 37 | func testStorePerformance() { 38 | measure { 39 | perfStore = zip(perfSeq, perfSeq.dropFirst(1)).reduce(into: PerfStore()) { result, pair in 40 | result[pair.0, default: [:]][pair.1, default: 0] += 1 41 | } 42 | } 43 | } 44 | } 45 | --------------------------------------------------------------------------------