From 5e958cca47fc4ea0a8cb0a0f920c1e6386b0f1f3 Mon Sep 17 00:00:00 2001
From: Colin DAMON <cdamon@ippon.fr>
Date: Mon, 6 Sep 2021 20:18:31 +0200
Subject: [PATCH] Markov chain implementation

---
 .gitlab-ci.yml                                |   1 +
 markov-chain/.gitlab-ci.yml                   |  11 ++
 markov-chain/README.md                        |   9 ++
 markov-chain/pom.xml                          |  29 +++++
 .../java/fr/ippon/markov/MarkovChain.java     |  39 +++++++
 .../src/main/java/fr/ippon/markov/Stat.java   |   5 +
 .../main/java/fr/ippon/markov/WordStats.java  |  28 +++++
 .../java/fr/ippon/markov/MarkovChainTest.java | 106 ++++++++++++++++++
 readme.md                                     |   1 +
 9 files changed, 229 insertions(+)
 create mode 100644 markov-chain/.gitlab-ci.yml
 create mode 100644 markov-chain/README.md
 create mode 100644 markov-chain/pom.xml
 create mode 100644 markov-chain/src/main/java/fr/ippon/markov/MarkovChain.java
 create mode 100644 markov-chain/src/main/java/fr/ippon/markov/Stat.java
 create mode 100644 markov-chain/src/main/java/fr/ippon/markov/WordStats.java
 create mode 100644 markov-chain/src/test/java/fr/ippon/markov/MarkovChainTest.java

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0c16d1af..0b825bb3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -40,3 +40,4 @@ include:
   - local: "/java-diamond/.gitlab-ci.yml"
   - local: "/diamond-ts/.gitlab-ci.yml"
   - local: "/java-memoizers/.gitlab-ci.yml"
+  - local: "/markov-chain/.gitlab-ci.yml"
diff --git a/markov-chain/.gitlab-ci.yml b/markov-chain/.gitlab-ci.yml
new file mode 100644
index 00000000..47fe1677
--- /dev/null
+++ b/markov-chain/.gitlab-ci.yml
@@ -0,0 +1,11 @@
+package-markov-chain:
+  variables:
+    PROJECT_FOLDER: "markov-chain"
+  extends: .java
+  only:
+    refs:
+      - master
+      - merge_requests
+    changes:
+      - ".gitlab-common-ci.yml"
+      - "markov-chain/**/*"
diff --git a/markov-chain/README.md b/markov-chain/README.md
new file mode 100644
index 00000000..ba4c301c
--- /dev/null
+++ b/markov-chain/README.md
@@ -0,0 +1,9 @@
+# Markov Chain
+
+Découverte du kata [Markov Chain](https://codingdojo.org/kata/MarkovChain/)
+
+-   **Auteurs** : Anthony REY et Colin DAMON
+-   **Date** : 06/09/2021
+-   **Langage** : Java
+-   **Niveau** : Moyen
+-   **Replay** : [Twitch](https://www.twitch.tv/videos/1140906031)
diff --git a/markov-chain/pom.xml b/markov-chain/pom.xml
new file mode 100644
index 00000000..dc530323
--- /dev/null
+++ b/markov-chain/pom.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <version>1.0.0</version>
+    <groupId>fr.ippon.kata</groupId>
+    <artifactId>java-parent</artifactId>
+    <relativePath>../java-parent</relativePath>
+  </parent>
+
+  <version>1.0.0-SNAPSHOT</version>
+  <artifactId>markov-chain</artifactId>
+
+  <name>MarkovChain</name>
+
+  <developers>
+    <developer>
+      <email>arey@ippon.fr</email>
+      <name>Anthony REY</name>
+    </developer>
+    <developer>
+      <email>cdamon@ippon.fr</email>
+      <name>Colin DAMON</name>
+    </developer>
+  </developers>
+</project>
diff --git a/markov-chain/src/main/java/fr/ippon/markov/MarkovChain.java b/markov-chain/src/main/java/fr/ippon/markov/MarkovChain.java
new file mode 100644
index 00000000..439277f7
--- /dev/null
+++ b/markov-chain/src/main/java/fr/ippon/markov/MarkovChain.java
@@ -0,0 +1,39 @@
+package fr.ippon.markov;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class MarkovChain {
+
+  private final Map<String, WordStats> stats = new ConcurrentHashMap<>();
+
+  public void learn(String text) {
+    String[] words = text.split(" ");
+    for (int i = 0; i < words.length - 1; i++) {
+      String current = words[i];
+      String next = words[i + 1];
+
+      append(current, next);
+    }
+  }
+
+  private void append(String current, String next) {
+    stats
+        .computeIfAbsent(current.toLowerCase(),
+            this::newWordStats)
+        .add(next);
+  }
+
+  private WordStats newWordStats(String dummy) {
+    return new WordStats();
+  }
+
+  public WordStats stats(String word) {
+    return stats.getOrDefault(word.toLowerCase(),
+        new WordStats());
+  }
+
+  public Map<String, WordStats> stats() {
+    return stats;
+  }
+}
diff --git a/markov-chain/src/main/java/fr/ippon/markov/Stat.java b/markov-chain/src/main/java/fr/ippon/markov/Stat.java
new file mode 100644
index 00000000..92c0afaa
--- /dev/null
+++ b/markov-chain/src/main/java/fr/ippon/markov/Stat.java
@@ -0,0 +1,5 @@
+package fr.ippon.markov;
+
+public record Stat(String word, float percentage) {
+
+}
diff --git a/markov-chain/src/main/java/fr/ippon/markov/WordStats.java b/markov-chain/src/main/java/fr/ippon/markov/WordStats.java
new file mode 100644
index 00000000..a6549397
--- /dev/null
+++ b/markov-chain/src/main/java/fr/ippon/markov/WordStats.java
@@ -0,0 +1,28 @@
+package fr.ippon.markov;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class WordStats {
+
+  private final AtomicInteger size = new AtomicInteger();
+  private final Map<String, AtomicInteger> stats = new ConcurrentHashMap<>();
+
+  public void add(String word) {
+    size.incrementAndGet();
+    stats.computeIfAbsent(word, key -> new AtomicInteger())
+        .incrementAndGet();
+  }
+
+  public Collection<Stat> get() {
+    return stats.entrySet()
+        .stream()
+        .map(entry -> new Stat(entry.getKey(),
+            entry.getValue()
+                .floatValue() / size.floatValue()))
+        .toList();
+  }
+
+}
diff --git a/markov-chain/src/test/java/fr/ippon/markov/MarkovChainTest.java b/markov-chain/src/test/java/fr/ippon/markov/MarkovChainTest.java
new file mode 100644
index 00000000..8c9ff21c
--- /dev/null
+++ b/markov-chain/src/test/java/fr/ippon/markov/MarkovChainTest.java
@@ -0,0 +1,106 @@
+package fr.ippon.markov;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+class MarkovChainTest {
+  private final MarkovChain markov = new MarkovChain();
+
+  @Test
+  void shouldGetStatsForOneWord() {
+    markov.learn("Les");
+
+    assertThat(markov.stats("Les")
+        .get()).isEmpty();
+  }
+
+  @Test
+  void shoulGetStatsForTwoWords() {
+    markov.learn("Les hommes");
+
+    assertThat(markov.stats("Les")
+        .get()).containsExactly(stat("hommes", 1f));
+  }
+
+  @Test
+  void shoulGetStatsWithMultipleFollowers() {
+    markov.learn("Les hommes les plus grands");
+
+    assertThat(markov.stats("Les")
+        .get()).containsExactly(stat("hommes", 0.5f),
+            new Stat("plus", 0.5f));
+    assertThat(markov.stats("hommes")
+        .get()).containsExactly(stat("les", 1f));
+  }
+
+  @Test
+  void shoulGetStats() {
+    markov.learn("Les hommes les plus grands");
+
+    assertThatStats(markov.stats()).hasWord("les")
+        .withStat("hommes", 0.5f)
+        .withStat("plus", 0.5f)
+        .and()
+        .hasWord("hommes")
+        .withStat("les", 1f);
+  }
+
+  private static Stat stat(String word, float stat) {
+    return new Stat(word, stat);
+  }
+
+  private static StatsAsserter assertThatStats(
+      Map<String, WordStats> stats) {
+    return new StatsAsserter(stats);
+  }
+
+  private static class StatsAsserter {
+
+    private final Map<String, WordStats> stats;
+
+    public StatsAsserter(Map<String, WordStats> stats) {
+      this.stats = stats;
+    }
+
+    public WordStatAsserter hasWord(String word) {
+      assertThat(stats).containsKey(word);
+
+      return new WordStatAsserter(stats.get(word), this);
+    }
+
+  }
+
+  private static class WordStatAsserter {
+
+    private final WordStats wordStats;
+    private final StatsAsserter source;
+
+    public WordStatAsserter(WordStats wordStats,
+        StatsAsserter source) {
+      this.wordStats = wordStats;
+      this.source = source;
+    }
+
+    public WordStatAsserter withStat(String word,
+        float percent) {
+      float result = wordStats.get()
+          .stream()
+          .filter(stat -> stat.word()
+              .equals(word))
+          .findFirst()
+          .orElseThrow(AssertionError::new)
+          .percentage();
+
+      assertThat(result).isEqualTo(percent);
+
+      return this;
+    }
+
+    public StatsAsserter and() {
+      return source;
+    }
+  }
+}
diff --git a/readme.md b/readme.md
index 92959e14..26510f98 100644
--- a/readme.md
+++ b/readme.md
@@ -44,6 +44,7 @@ Un kata de code est un petit exercice pensé pour s'entrainer jusqu'à maitriser
 -   [Tennis Refactoring kata](/tennis/refactoring)
 -   [Puzzles](java-puzzles)
 -   [Diamond](java-diamond)
+-   [Markov chain](markov-chain)
 
 ### Énervé
 
-- 
GitLab