From ea21fc92a25da8e6c583f96692f5519f322ba212 Mon Sep 17 00:00:00 2001
From: tina <lt-bloody@riox.eu>
Date: Wed, 22 Aug 2018 18:21:57 +0200
Subject: [PATCH] moves getCycleLength to corresponding files, adds function to
 get next menses (not finished) with first tests

---
 components/stats.js       |  14 +----
 lib/cycle-length.js       |  14 ++++-
 lib/cycle.js              |  33 +++++++++-
 test/cycle-length.spec.js |   2 +-
 test/cycle.spec.js        | 123 ++++++++++++++++++++++++++++++++++++++
 5 files changed, 171 insertions(+), 15 deletions(-)

diff --git a/components/stats.js b/components/stats.js
index f3f136c7..6ae0e271 100644
--- a/components/stats.js
+++ b/components/stats.js
@@ -4,10 +4,10 @@ import {
   View,
   ScrollView
 } from 'react-native'
-import { LocalDate, ChronoUnit } from 'js-joda'
+
 import styles from '../styles/index'
 import cycleModule from '../lib/cycle'
-import getCycleInfo from '../lib/cycle-length'
+import {getCycleLengthStats as getCycleInfo, getCycleLength} from '../lib/cycle-length'
 import {stats as labels} from './labels'
 
 export default class Stats extends Component {
@@ -56,14 +56,4 @@ export default class Stats extends Component {
       </ScrollView>
     )
   }
-}
-
-function getCycleLength(cycleStartDates) {
-  const cycleLengths = []
-  for (let i = 0; i < cycleStartDates.length - 1; i++) {
-    const nextCycleStart = LocalDate.parse(cycleStartDates[i])
-    const cycleStart = LocalDate.parse(cycleStartDates[i + 1])
-    cycleLengths.push(cycleStart.until(nextCycleStart, ChronoUnit.DAYS))
-  }
-  return cycleLengths
 }
\ No newline at end of file
diff --git a/lib/cycle-length.js b/lib/cycle-length.js
index 6f8feb56..1d4ae02b 100644
--- a/lib/cycle-length.js
+++ b/lib/cycle-length.js
@@ -1,6 +1,8 @@
 import assert from 'assert'
+import { LocalDate, ChronoUnit } from 'js-joda'
+import cycleModule from '../lib/cycle'
 
-export default function getCycleLengthStats(cycleLengths) {
+export function getCycleLengthStats(cycleLengths) {
   throwIfArgsAreNotInRequiredFormat(cycleLengths)
   const cycleLengthStats = {}
   const sortedCycleLengths = cycleLengths.sort((a, b) => {
@@ -46,4 +48,14 @@ function throwIfArgsAreNotInRequiredFormat(cycleLengths) {
     assert.equal(typeof cycleLength, 'number', 'Elements in the array should be of type number.')
     assert.ok(!isNaN(cycleLength), 'Elements of array should not be NaN.')
   })
+}
+
+export function getCycleLength(cycleStartDates) {
+  const cycleLengths = []
+  for (let i = 0; i < cycleStartDates.length - 1; i++) {
+    const nextCycleStart = LocalDate.parse(cycleStartDates[i])
+    const cycleStart = LocalDate.parse(cycleStartDates[i + 1])
+    cycleLengths.push(cycleStart.until(nextCycleStart, ChronoUnit.DAYS))
+  }
+  return cycleLengths
 }
\ No newline at end of file
diff --git a/lib/cycle.js b/lib/cycle.js
index 23bc3cda..affdee89 100644
--- a/lib/cycle.js
+++ b/lib/cycle.js
@@ -1,4 +1,5 @@
 import * as joda from 'js-joda'
+import {getCycleLengthStats, getCycleLength} from './cycle-length'
 const LocalDate = joda.LocalDate
 const DAYS = joda.ChronoUnit.DAYS
 
@@ -143,11 +144,41 @@ export default function config(opts) {
     }
   }
 
+  function getPredictedMenses(maxCycleLength, minCyclesForPrediction) {
+    maxCycleLength = maxCycleLength || 99
+    minCyclesForPrediction = minCyclesForPrediction || 3
+    const allMensesStarts = getAllMensesStarts()
+
+    const atLeastOneCycle = allMensesStarts.length > 1
+    if (!atLeastOneCycle ||
+      allMensesStarts.length < minCyclesForPrediction ||
+      getCycleDayNumber(LocalDate.now().toString()) > maxCycleLength
+    ) {
+      return {}
+    }
+    const cycleLengths = getCycleLength(allMensesStarts)
+    const cycleInfo = getCycleLengthStats(cycleLengths)
+    const periodDistance = Math.round(cycleInfo.mean)
+    const periodStartVariation = (cycleInfo.stdDeviation < 1.5) ? 1 : 2 // threshold is choosen a little arbitrarily
+    var lastStart = allMensesStarts[0]
+    const predictedMenses = []
+    for (let i = 0; i < 3; i++) {
+      lastStart = LocalDate.parse(lastStart).plusDays(periodDistance).toString()
+      const nextPredictedRange = {
+        'startDate': LocalDate.parse(lastStart).minusDays(periodStartVariation).toString(),
+        'endDate': LocalDate.parse(lastStart).plusDays(periodStartVariation).toString()
+      }
+      predictedMenses.push(nextPredictedRange)
+    }
+    return predictedMenses
+  }
+
   return {
     getCycleDayNumber,
     getCycleForDay,
     getPreviousCycle,
     getCyclesBefore,
-    getAllMensesStarts
+    getAllMensesStarts,
+    getPredictedMenses
   }
 }
\ No newline at end of file
diff --git a/test/cycle-length.spec.js b/test/cycle-length.spec.js
index 043cf96d..3714337a 100644
--- a/test/cycle-length.spec.js
+++ b/test/cycle-length.spec.js
@@ -1,7 +1,7 @@
 import chai from 'chai'
 import { AssertionError } from 'assert'
 
-import cycleInfo from '../lib/cycle-length'
+import {getCycleLengthStats as cycleInfo} from '../lib/cycle-length'
 
 const expect = chai.expect
 
diff --git a/test/cycle.spec.js b/test/cycle.spec.js
index a0efa7d4..09df48f5 100644
--- a/test/cycle.spec.js
+++ b/test/cycle.spec.js
@@ -343,4 +343,127 @@ describe('getCycleForDay', () => {
       },
     ])
   })
+})
+
+describe.only('getPredictedMenses', () => {
+  describe('cannot predict next menses', () => {
+    it('if no bleeding is documented', () => {
+      const cycleDaysSortedByDate = [ {} ]
+
+      const { getPredictedMenses } = cycleModule({
+        cycleDaysSortedByDate,
+        bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding)
+      })
+      const result = getPredictedMenses(99, 1)
+      expect(result).to.eql({})
+    })
+    it('if no cycle is completed', () => {
+      const cycleDaysSortedByDate = [
+        {
+          date: '2018-06-02',
+          bleeding: { value: 2 }
+        }
+      ]
+
+      const { getPredictedMenses } = cycleModule({
+        cycleDaysSortedByDate,
+        bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding)
+      })
+      const result = getPredictedMenses(99, 1)
+      expect(result).to.eql({})
+    })
+    it('if number of cycles is below minCyclesForPrediction', () => {
+      const cycleDaysSortedByDate = [
+        {
+          date: '2018-06-02',
+          bleeding: { value: 2 }
+        },
+        {
+          date: '2018-06-01',
+          bleeding: { value: 2 }
+        },
+        {
+          date: '2018-05-01',
+          bleeding: { value: 2 }
+        },
+      ]
+
+      const { getPredictedMenses } = cycleModule({
+        cycleDaysSortedByDate,
+        bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding)
+      })
+      const result = getPredictedMenses()
+      expect(result).to.eql({})
+    })
+    it('if last bleeding was more than maxCycleLength days ago', () => {
+      const cycleDaysSortedByDate = [
+        {
+          date: '2017-07-02',
+          bleeding: { value: 2 }
+        },
+        {
+          date: '2017-06-01',
+          bleeding: { value: 2 }
+        },
+        {
+          date: '2017-05-01',
+          bleeding: { value: 2 }
+        },
+        {
+          date: '2017-04-01',
+          bleeding: { value: 2 }
+        }
+      ]
+
+      const { getPredictedMenses } = cycleModule({
+        cycleDaysSortedByDate,
+        bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding)
+      })
+      const result = getPredictedMenses()
+      expect(result).to.eql({})
+    })
+  })
+  describe('works', () => {
+    it('if number of cycles is above minCyclesForPrediction with little standard deviation', () => {
+      const cycleDaysSortedByDate = [
+        {
+          date: '2018-08-02',
+          bleeding: { value: 2 }
+        },
+        {
+          date: '2018-07-02',
+          bleeding: { value: 2 }
+        },
+        {
+          date: '2018-06-01',
+          bleeding: { value: 2 }
+        },
+        {
+          date: '2018-05-01',
+          bleeding: { value: 2 }
+        },
+      ]
+
+      const { getPredictedMenses } = cycleModule({
+        cycleDaysSortedByDate,
+        bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding)
+      })
+      const result = getPredictedMenses()
+      const expectedResult = [
+        {
+          'startDate': '2018-09-01',
+          'endDate': '2018-09-03'
+        },
+        {
+          'startDate': '2018-10-02',
+          'endDate': '2018-10-04'
+        },
+        {
+          'startDate': '2018-11-02',
+          'endDate': '2018-11-04'
+        }
+      ]
+      expect(result).to.eql(expectedResult)
+    })
+  })
 })
\ No newline at end of file
-- 
GitLab