diff --git a/components/stats.js b/components/stats.js index f3f136c78b2cbf5da4238590a7f76dad7b979526..6ae0e271f992843363c4ddeb57f40a3c89ad36a1 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 6f8feb56f54f4ecd70bafe7fe9648133314178cb..1d4ae02b31c40c054a6db70601e0328942fee6e0 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 23bc3cda757e3f8934cc06c85e12c3445906bdaa..affdee8977ae3a322df3560452fc2d41afff871d 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 043cf96d34a355993549b1c400e99bb509645d63..3714337a8b6ee27285200cf4a5f068d6ed94ae4f 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 a0efa7d45cec8b562a0fac6d2df6c36c1ead3f75..09df48f5873b37d524f068b7f6428059b0f5c6ae 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