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