├── .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 |

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 |

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 |
--------------------------------------------------------------------------------