diff --git a/components/stats.js b/components/stats.js index 6ae0e27..ad4b0e6 100644 --- a/components/stats.js +++ b/components/stats.js @@ -7,7 +7,7 @@ import { import styles from '../styles/index' import cycleModule from '../lib/cycle' -import {getCycleLengthStats as getCycleInfo, getCycleLength} from '../lib/cycle-length' +import {getCycleLengthStats as getCycleInfo} from '../lib/cycle-length' import {stats as labels} from './labels' export default class Stats extends Component { @@ -18,7 +18,7 @@ export default class Stats extends Component { let numberOfCycles let cycleInfo if (atLeastOneCycle) { - cycleLengths = getCycleLength(allMensesStarts) + cycleLengths = cycleModule().getCycleLength(allMensesStarts) numberOfCycles = cycleLengths.length if (numberOfCycles > 1) { cycleInfo = getCycleInfo(cycleLengths) @@ -31,10 +31,14 @@ export default class Stats extends Component { {labels.emptyStats} } {atLeastOneCycle && numberOfCycles === 1 && - {labels.oneCycleStats(cycleLengths[0])} + + {labels.oneCycleStats(cycleLengths[0])} + } {atLeastOneCycle && numberOfCycles > 1 && - {labels.getBasisOfStats(numberOfCycles)} + + {labels.getBasisOfStats(numberOfCycles)} + {labels.averageLabel} {cycleInfo.mean + ' ' + labels.daysLabel} diff --git a/lib/cycle-length.js b/lib/cycle-length.js index 1d4ae02..956328d 100644 --- a/lib/cycle-length.js +++ b/lib/cycle-length.js @@ -1,6 +1,4 @@ import assert from 'assert' -import { LocalDate, ChronoUnit } from 'js-joda' -import cycleModule from '../lib/cycle' export function getCycleLengthStats(cycleLengths) { throwIfArgsAreNotInRequiredFormat(cycleLengths) @@ -48,14 +46,4 @@ 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 affdee8..5d41855 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -1,5 +1,5 @@ import * as joda from 'js-joda' -import {getCycleLengthStats, getCycleLength} from './cycle-length' +import {getCycleLengthStats} from './cycle-length' const LocalDate = joda.LocalDate const DAYS = joda.ChronoUnit.DAYS @@ -7,6 +7,8 @@ export default function config(opts) { let bleedingDaysSortedByDate let cycleDaysSortedByDate let maxBreakInBleeding + let maxCycleLength + let minCyclesForPrediction if (!opts) { // we only want to require (and run) the db module @@ -14,10 +16,14 @@ export default function config(opts) { bleedingDaysSortedByDate = require('../db').bleedingDaysSortedByDate cycleDaysSortedByDate = require('../db').cycleDaysSortedByDate maxBreakInBleeding = 1 + maxCycleLength = 99 + minCyclesForPrediction = 3 } else { bleedingDaysSortedByDate = opts.bleedingDaysSortedByDate || [] cycleDaysSortedByDate = opts.cycleDaysSortedByDate || [] maxBreakInBleeding = opts.maxBreakInBleeding || 1 + maxCycleLength = opts.maxCycleLength || 99 + minCyclesForPrediction = opts.minCyclesForPrediction || 3 } function getLastMensesStart(targetDateString) { @@ -144,22 +150,37 @@ export default function config(opts) { } } - function getPredictedMenses(maxCycleLength, minCyclesForPrediction) { - maxCycleLength = maxCycleLength || 99 - minCyclesForPrediction = minCyclesForPrediction || 3 + 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]) + const cycleLength = cycleStart.until(nextCycleStart, DAYS) + if (cycleLength <= maxCycleLength) { cycleLengths.push(cycleLength) } + } + return cycleLengths + } + + function getPredictedMenses() { const allMensesStarts = getAllMensesStarts() const atLeastOneCycle = allMensesStarts.length > 1 if (!atLeastOneCycle || - allMensesStarts.length < minCyclesForPrediction || - getCycleDayNumber(LocalDate.now().toString()) > maxCycleLength + allMensesStarts.length < minCyclesForPrediction ) { 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 + let periodStartVariation + if (cycleInfo.stdDeviation === null) { + periodStartVariation = 2 + } else if (cycleInfo.stdDeviation < 1.5) { // threshold is choosen a little arbitrarily + periodStartVariation = 1 + } else { + periodStartVariation = 2 + } var lastStart = allMensesStarts[0] const predictedMenses = [] for (let i = 0; i < 3; i++) { @@ -179,6 +200,7 @@ export default function config(opts) { getPreviousCycle, getCyclesBefore, getAllMensesStarts, + getCycleLength, getPredictedMenses } } \ No newline at end of file diff --git a/test/cycle.spec.js b/test/cycle.spec.js index 09df48f..34ed144 100644 --- a/test/cycle.spec.js +++ b/test/cycle.spec.js @@ -345,19 +345,21 @@ describe('getCycleForDay', () => { }) }) -describe.only('getPredictedMenses', () => { +describe('getPredictedMenses', () => { describe('cannot predict next menses', () => { it('if no bleeding is documented', () => { const cycleDaysSortedByDate = [ {} ] const { getPredictedMenses } = cycleModule({ cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding), + maxCycleLength: 99, + minCyclesForPrediction: 1 }) - const result = getPredictedMenses(99, 1) + const result = getPredictedMenses() expect(result).to.eql({}) }) - it('if no cycle is completed', () => { + it('if one bleeding is documented (no completed cycle)', () => { const cycleDaysSortedByDate = [ { date: '2018-06-02', @@ -367,9 +369,11 @@ describe.only('getPredictedMenses', () => { const { getPredictedMenses } = cycleModule({ cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding), + maxCycleLength: 99, + minCyclesForPrediction: 1 }) - const result = getPredictedMenses(99, 1) + const result = getPredictedMenses() expect(result).to.eql({}) }) it('if number of cycles is below minCyclesForPrediction', () => { @@ -395,36 +399,43 @@ describe.only('getPredictedMenses', () => { const result = getPredictedMenses() expect(result).to.eql({}) }) - it('if last bleeding was more than maxCycleLength days ago', () => { + }) + describe('works', () => { + it('for one completed cycle with minCyclesForPrediction = 1', () => { const cycleDaysSortedByDate = [ { - date: '2017-07-02', + date: '2018-07-15', bleeding: { value: 2 } }, { - date: '2017-06-01', - bleeding: { value: 2 } - }, - { - date: '2017-05-01', - bleeding: { value: 2 } - }, - { - date: '2017-04-01', + date: '2018-07-01', bleeding: { value: 2 } } ] const { getPredictedMenses } = cycleModule({ cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding), + minCyclesForPrediction: 1 }) const result = getPredictedMenses() - expect(result).to.eql({}) + const expectedResult = [ + { + 'startDate': '2018-07-27', + 'endDate': '2018-07-31' + }, + { + 'startDate': '2018-08-10', + 'endDate': '2018-08-14' + }, + { + 'startDate': '2018-08-24', + 'endDate': '2018-08-28' + } + ] + expect(result).to.eql(expectedResult) }) - }) - describe('works', () => { - it('if number of cycles is above minCyclesForPrediction with little standard deviation', () => { + it('if number of cycles is above minCyclesForPrediction', () => { const cycleDaysSortedByDate = [ { date: '2018-08-02', @@ -465,5 +476,87 @@ describe.only('getPredictedMenses', () => { ] expect(result).to.eql(expectedResult) }) + it('3 cycles with little standard deviation', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-08-01', + bleeding: { value: 2 } + }, + { + date: '2018-07-18', + bleeding: { value: 2 } + }, + { + date: '2018-07-05', + bleeding: { value: 2 } + }, + { + date: '2018-06-20', + bleeding: { value: 2 } + }, + ] + + const { getPredictedMenses } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const result = getPredictedMenses() + const expectedResult = [ + { + 'startDate': '2018-08-14', + 'endDate': '2018-08-16' + }, + { + 'startDate': '2018-08-28', + 'endDate': '2018-08-30' + }, + { + 'startDate': '2018-09-11', + 'endDate': '2018-09-13' + } + ] + expect(result).to.eql(expectedResult) + }) + it('3 cycles with bigger standard deviation', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-08-01', + bleeding: { value: 2 } + }, + { + date: '2018-07-14', + bleeding: { value: 2 } + }, + { + date: '2018-07-04', + bleeding: { value: 2 } + }, + { + date: '2018-06-20', + bleeding: { value: 2 } + }, + ] + + const { getPredictedMenses } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const result = getPredictedMenses() + const expectedResult = [ + { + 'startDate': '2018-08-13', + 'endDate': '2018-08-17' + }, + { + 'startDate': '2018-08-27', + 'endDate': '2018-08-31' + }, + { + 'startDate': '2018-09-10', + 'endDate': '2018-09-14' + } + ] + expect(result).to.eql(expectedResult) + }) }) }) \ No newline at end of file