From 2b2e24bb5c604f5c6cc7c27350401a0a6a962a28 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 23 Oct 2018 16:24:17 +0200 Subject: [PATCH 01/12] Add isMensesStart --- lib/cycle.js | 22 ++++++- test/cycle.spec.js | 142 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) diff --git a/lib/cycle.js b/lib/cycle.js index 5393eb9..28f9867 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -133,6 +133,25 @@ export default function config(opts) { } } + function isMensesStart(cycleDay) { + if (!cycleDay.bleeding || cycleDay.bleeding.exclude) return false + const bleedingDays = bleedingDaysSortedByDate + if (noBleedingDayWithinThreshold(cycleDay)) { + return true + } + return false + + function noBleedingDayWithinThreshold(day) { + const localDate = LocalDate.parse(day.date) + const threshold = localDate.minusDays(maxBreakInBleeding + 1).toString() + const index = bleedingDays.findIndex(day => day.date === cycleDay.date) + const previousBleedingDays = bleedingDays.slice(index + 1) + return !previousBleedingDays.some(day => { + return day.date >= threshold && !day.bleeding.exclude + }) + } + } + function getCycleLength(cycleStartDates) { const cycleLengths = [] for (let i = 0; i < cycleStartDates.length - 1; i++) { @@ -188,6 +207,7 @@ export default function config(opts) { getCyclesBefore, getAllMensesStarts, getCycleLength, - getPredictedMenses + getPredictedMenses, + isMensesStart } } \ No newline at end of file diff --git a/test/cycle.spec.js b/test/cycle.spec.js index d61ee8b..7a7f393 100644 --- a/test/cycle.spec.js +++ b/test/cycle.spec.js @@ -685,4 +685,146 @@ describe('getAllMensesStart', () => { expect(result.length).to.eql(500) expect(duration).to.be.lessThan(100) }) +}) + +describe('isMensesStart', () => { + it('works for simple menses start', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-05-04', + }, + { + date: '2018-05-03', + bleeding: { value: 1 } + }, + { + date: '2018-05-02', + bleeding: { value: 1 } + }, + { + date: '2018-05-01', + bleeding: { value: 1 } + }, + { + date: '2018-04-30', + } + ] + + const { isMensesStart } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const start = isMensesStart(cycleDaysSortedByDate[3]) + expect(start).to.be.true() + expect(isMensesStart(cycleDaysSortedByDate[0])).to.be.false() + expect(isMensesStart(cycleDaysSortedByDate[1])).to.be.false() + expect(isMensesStart(cycleDaysSortedByDate[2])).to.be.false() + expect(isMensesStart(cycleDaysSortedByDate[4])).to.be.false() + }) + + it('works with previous excluded value', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-06-01', + bleeding: { value: 2 } + }, + { + date: '2018-05-01', + bleeding: { value: 2 } + }, + { + date: '2018-04-30', + bleeding: { value: 2 , exclude: true} + }, + ] + + const { isMensesStart } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const start = isMensesStart(cycleDaysSortedByDate[1]) + expect(start).to.be.true() + const notStart = isMensesStart(cycleDaysSortedByDate[2]) + expect(notStart).to.be.false() + }) + + it('returns false when day has no bleeding', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-06-01', + }, + { + date: '2018-05-01', + }, + { + date: '2018-04-30', + bleeding: { value: 2 , exclude: true} + }, + ] + + const { isMensesStart } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const start = isMensesStart(cycleDaysSortedByDate[0]) + expect(start).to.be.false() + }) + + it('returns false when there is a previous bleeding day within the threshold', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-06-01', + }, + { + date: '2018-05-01', + }, + { + date: '2018-04-30', + bleeding: { value: 2 } + }, + { + date: '2018-04-29' + }, + { + date: '2018-04-28', + bleeding: { value: 2 } + }, + ] + + const { isMensesStart } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const start = isMensesStart(cycleDaysSortedByDate[2]) + expect(start).to.be.false() + }) + + it('returns true when there is a previous excluded bleeding day within the threshold', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-06-01', + }, + { + date: '2018-05-01', + }, + { + date: '2018-04-30', + bleeding: { value: 2 } + }, + { + date: '2018-04-29' + }, + { + date: '2018-04-28', + bleeding: { value: 2 , exclude: true} + }, + ] + + const { isMensesStart } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const start = isMensesStart(cycleDaysSortedByDate[2]) + expect(start).to.be.true() + }) }) \ No newline at end of file From c2c0e0c6a2445193314620d210b444ea54c9f195 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 23 Oct 2018 17:38:26 +0200 Subject: [PATCH 02/12] Add getMensesDaysAfter --- lib/cycle.js | 41 ++++++++--- test/cycle.spec.js | 172 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 9 deletions(-) diff --git a/lib/cycle.js b/lib/cycle.js index 28f9867..042cff2 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -135,23 +135,45 @@ export default function config(opts) { function isMensesStart(cycleDay) { if (!cycleDay.bleeding || cycleDay.bleeding.exclude) return false - const bleedingDays = bleedingDaysSortedByDate - if (noBleedingDayWithinThreshold(cycleDay)) { - return true - } + if (noBleedingDayWithinThresholdBefore(cycleDay)) return true return false - function noBleedingDayWithinThreshold(day) { - const localDate = LocalDate.parse(day.date) + function noBleedingDayWithinThresholdBefore(cycleDay) { + const localDate = LocalDate.parse(cycleDay.date) const threshold = localDate.minusDays(maxBreakInBleeding + 1).toString() + const bleedingDays = bleedingDaysSortedByDate const index = bleedingDays.findIndex(day => day.date === cycleDay.date) - const previousBleedingDays = bleedingDays.slice(index + 1) - return !previousBleedingDays.some(day => { + const candidates = bleedingDays.slice(index + 1) + return !candidates.some(day => { return day.date >= threshold && !day.bleeding.exclude }) } } + function getMensesDaysAfter(bleedingDay) { + const bleedingDays = bleedingDaysSortedByDate.filter(d => { + return !d.bleeding.exclude + }) + const startIndex = bleedingDays.findIndex(day => { + return day.date === bleedingDay.date + }) + return recurse(bleedingDay, startIndex, []) + + function recurse(day, i, mensesDays) { + if (i === 0) return mensesDays + const next = bleedingDays[i - 1] + if (!isWithinThreshold(day, next)) return mensesDays + mensesDays.unshift(next) + return recurse(next, i - 1, mensesDays) + } + + function isWithinThreshold(cycleDay, nextCycleDay) { + const localDate = LocalDate.parse(cycleDay.date) + const threshold = localDate.plusDays(maxBreakInBleeding + 1).toString() + return nextCycleDay.date <= threshold + } + } + function getCycleLength(cycleStartDates) { const cycleLengths = [] for (let i = 0; i < cycleStartDates.length - 1; i++) { @@ -208,6 +230,7 @@ export default function config(opts) { getAllMensesStarts, getCycleLength, getPredictedMenses, - isMensesStart + isMensesStart, + getMensesDaysAfter } } \ No newline at end of file diff --git a/test/cycle.spec.js b/test/cycle.spec.js index 7a7f393..d51392a 100644 --- a/test/cycle.spec.js +++ b/test/cycle.spec.js @@ -827,4 +827,176 @@ describe('isMensesStart', () => { const start = isMensesStart(cycleDaysSortedByDate[2]) expect(start).to.be.true() }) +}) + +describe('getMensesDaysAfter', () => { + it('works for simple menses start', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-05-04', + }, + { + date: '2018-05-03', + bleeding: { value: 1 } + }, + { + date: '2018-05-02', + bleeding: { value: 1 } + }, + { + date: '2018-05-01', + bleeding: { value: 1 } + }, + { + date: '2018-04-30', + } + ] + + const { getMensesDaysAfter } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const days = getMensesDaysAfter(cycleDaysSortedByDate[3]) + expect(days).to.eql([ + { + date: '2018-05-03', + bleeding: { value: 1 } + }, + { + date: '2018-05-02', + bleeding: { value: 1 } + } + ]) + }) + + it('ignores excluded values', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-05-04', + }, + { + date: '2018-05-03', + bleeding: { value: 1 } + }, + { + date: '2018-05-02', + bleeding: { value: 1, exclude: true } + }, + { + date: '2018-05-01', + bleeding: { value: 1 } + }, + { + date: '2018-04-30', + } + ] + + const { getMensesDaysAfter } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const days = getMensesDaysAfter(cycleDaysSortedByDate[3]) + expect(days).to.eql([ + { + date: '2018-05-03', + bleeding: { value: 1 } + } + ]) + }) + + it('returns empty when there are no bleeding days after', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-05-04', + }, + { + date: '2018-05-03', + }, + { + date: '2018-05-02', + }, + { + date: '2018-05-01', + bleeding: { value: 1 } + }, + { + date: '2018-04-30', + } + ] + + const { getMensesDaysAfter } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const days = getMensesDaysAfter(cycleDaysSortedByDate[3]) + expect(days).to.eql([]) + }) + + it('returns empty when there are no bleeding days within threshold', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-05-04', + bleeding: { value: 1 } + }, + { + date: '2018-05-03', + }, + { + date: '2018-05-02', + }, + { + date: '2018-05-01', + bleeding: { value: 1 } + }, + { + date: '2018-04-30', + } + ] + + const { getMensesDaysAfter } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const days = getMensesDaysAfter(cycleDaysSortedByDate[3]) + expect(days).to.eql([]) + }) + + it('includes days within the treshold', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-05-04', + }, + { + date: '2018-05-05', + bleeding: { value: 1 } + }, + { + date: '2018-05-03', + bleeding: { value: 1 } + }, + { + date: '2018-05-01', + bleeding: { value: 1 } + }, + { + date: '2018-04-30', + } + ] + + const { getMensesDaysAfter } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const days = getMensesDaysAfter(cycleDaysSortedByDate[3]) + expect(days).to.eql([ + { + date: '2018-05-05', + bleeding: { value: 1 } + }, + { + date: '2018-05-03', + bleeding: { value: 1 } + } + ]) + }) }) \ No newline at end of file From ca4cccfd650b0ebd17931534a7b9ab319aae3330 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 23 Oct 2018 19:42:51 +0200 Subject: [PATCH 03/12] Set isCycleStart when saving symptom --- db/index.js | 52 +++++++++++++- db/schemas/2.js | 166 ++++++++++++++++++++++++++++++++++++++++++++ db/schemas/index.js | 3 +- lib/cycle.js | 6 +- 4 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 db/schemas/2.js diff --git a/db/index.js b/db/index.js index ce0212a..d060896 100644 --- a/db/index.js +++ b/db/index.js @@ -4,8 +4,11 @@ import nodejs from 'nodejs-mobile-react-native' import fs from 'react-native-fs' import restart from 'react-native-restart' import schemas from './schemas' +import cycleModule from '../lib/cycle' let db +let isMensesStart +let getMensesDaysAfter export async function openDb ({ hash, persistConnection }) { const realmConfig = {} @@ -32,6 +35,9 @@ export async function openDb ({ hash, persistConnection }) { )) if (persistConnection) db = connection + const cycle = cycleModule() + isMensesStart = cycle.isMensesStart + getMensesDaysAfter = cycle.getMensesDaysAfter } @@ -47,16 +53,58 @@ export function getCycleDaysSortedByDate() { export function saveSymptom(symptom, cycleDay, val) { db.write(() => { - cycleDay[symptom] = val + if (symptom === 'bleeding') { + saveBleeding(cycleDay, val) + } else { + cycleDay[symptom] = val + } }) } +export function saveBleeding(cycleDay, bleeding) { + if (!bleeding) { + updateCycleDayAndMaybeSetNewCycleStart(cycleDay, bleeding) + } else { + cycleDay.bleeding = bleeding + cycleDay.isCycleStart = isMensesStart(cycleDay) + maybeClearOldCycleStartsInThisMenses(cycleDay) + } + + function updateCycleDayAndMaybeSetNewCycleStart(oldCycleDay, newValue) { + // if a bleeding value is deleted, we need to check if + // there are any following bleeding days and if the + // next one of them is now a cycle start + + // in order to get the menses days, the cycle day in question still + // has to have a bleeding value, so we get those days first and only + // then update the cycle day + const mensesDaysAfter = getMensesDaysAfter(oldCycleDay) + oldCycleDay.bleeding = newValue + + if (!mensesDaysAfter.length) return + + const nextOne = mensesDaysAfter[mensesDaysAfter.length - 1] + if (isMensesStart(nextOne)) { + nextOne.isCycleStart = true + } + } + + function maybeClearOldCycleStartsInThisMenses(cycleDay) { + // if we have a new bleeding day, we need to clear the + // menses start marker from all following days of this + // menses that may have been marked as start before + const mensesDaysAfter = getMensesDaysAfter(cycleDay) + mensesDaysAfter.forEach(day => day.isCycleStart = false) + } +} + export function getOrCreateCycleDay(localDate) { let result = db.objectForPrimaryKey('CycleDay', localDate) if (!result) { db.write(() => { result = db.create('CycleDay', { - date: localDate + date: localDate, + isCycleStart: false }) }) } diff --git a/db/schemas/2.js b/db/schemas/2.js new file mode 100644 index 0000000..6bf5cfb --- /dev/null +++ b/db/schemas/2.js @@ -0,0 +1,166 @@ +import cycleModule from '../../lib/cycle' + +const TemperatureSchema = { + name: 'Temperature', + properties: { + value: 'double', + exclude: 'bool', + time: { + type: 'string', + optional: true + }, + note: { + type: 'string', + optional: true + } + } +} + +const BleedingSchema = { + name: 'Bleeding', + properties: { + value: 'int', + exclude: 'bool' + } +} + +const MucusSchema = { + name: 'Mucus', + properties: { + feeling: 'int', + texture: 'int', + value: 'int', + exclude: 'bool' + } +} + +const CervixSchema = { + name: 'Cervix', + properties: { + opening: 'int', + firmness: 'int', + position: {type: 'int', optional: true }, + exclude: 'bool' + } +} + +const NoteSchema = { + name: 'Note', + properties: { + value: 'string' + } +} + +const DesireSchema = { + name: 'Desire', + properties: { + value: 'int' + } +} + +const SexSchema = { + name: 'Sex', + properties: { + solo: { type: 'bool', optional: true }, + partner: { type: 'bool', optional: true }, + condom: { type: 'bool', optional: true }, + pill: { type: 'bool', optional: true }, + iud: { type: 'bool', optional: true }, + patch: { type: 'bool', optional: true }, + ring: { type: 'bool', optional: true }, + implant: { type: 'bool', optional: true }, + diaphragm: { type: 'bool', optional: true }, + none: { type: 'bool', optional: true }, + other: { type: 'bool', optional: true }, + note: { type: 'string', optional: true } + } +} + +const PainSchema = { + name: 'Pain', + properties: { + cramps: { type: 'bool', optional: true }, + ovulationPain: { type: 'bool', optional: true }, + headache: { type: 'bool', optional: true }, + backache: { type: 'bool', optional: true }, + nausea: { type: 'bool', optional: true }, + tenderBreasts: { type: 'bool', optional: true }, + migraine: { type: 'bool', optional: true }, + other: { type: 'bool', optional: true }, + note: { type: 'string', optional: true } + } +} + +const CycleDaySchema = { + name: 'CycleDay', + primaryKey: 'date', + properties: { + date: 'string', + temperature: { + type: 'Temperature', + optional: true + }, + isCycleStart: 'bool', + bleeding: { + type: 'Bleeding', + optional: true + }, + mucus: { + type: 'Mucus', + optional: true + }, + cervix: { + type: 'Cervix', + optional: true + }, + note: { + type: 'Note', + optional: true + }, + desire: { + type: 'Desire', + optional: true + }, + sex: { + type: 'Sex', + optional: true + }, + pain: { + type: 'Pain', + optional: true + } + } +} + +export default { + schema: [ + CycleDaySchema, + TemperatureSchema, + BleedingSchema, + MucusSchema, + CervixSchema, + NoteSchema, + DesireSchema, + SexSchema, + PainSchema + ], + schemaVersion: 2, + migration: (oldRealm, newRealm) => { + if (oldRealm.schemaVersion >= 2) return + const oldBleedingDays = oldRealm.objects('CycleDay') + .filtered('bleeding != null') + .sorted('date', true) + + const { isMensesStart } = cycleModule({ + bleedingDaysSortedByDate: oldBleedingDays + }) + + const newBleedingDays = newRealm.objects('CycleDay') + .filtered('bleeding != null') + .sorted('date', true) + + oldBleedingDays.forEach((day, i) => { + newBleedingDays[i].isCycleStart = isMensesStart(day) + }) + } +} diff --git a/db/schemas/index.js b/db/schemas/index.js index eacf7dd..5b80738 100644 --- a/db/schemas/index.js +++ b/db/schemas/index.js @@ -1,4 +1,5 @@ import schema0 from './0.js' import schema1 from './1.js' +import schema2 from './2.js' -export default [schema0, schema1] \ No newline at end of file +export default [schema0, schema1, schema2] \ No newline at end of file diff --git a/lib/cycle.js b/lib/cycle.js index 042cff2..f565ed5 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -150,14 +150,14 @@ export default function config(opts) { } } - function getMensesDaysAfter(bleedingDay) { + function getMensesDaysAfter(cycleDay) { const bleedingDays = bleedingDaysSortedByDate.filter(d => { return !d.bleeding.exclude }) const startIndex = bleedingDays.findIndex(day => { - return day.date === bleedingDay.date + return day.date === cycleDay.date }) - return recurse(bleedingDay, startIndex, []) + return recurse(cycleDay, startIndex, []) function recurse(day, i, mensesDays) { if (i === 0) return mensesDays From 9188d847976f92d164f0f7a30cbe4695a6185948 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 25 Oct 2018 09:18:25 +0200 Subject: [PATCH 04/12] Use new isCycleStartFunctions throughout cycle module --- components/stats.js | 5 +- db/index.js | 4 + lib/cycle.js | 139 ++++++---------- test/cycle.spec.js | 383 +++++++++++++++++++------------------------- 4 files changed, 218 insertions(+), 313 deletions(-) diff --git a/components/stats.js b/components/stats.js index b02e03f..a888de4 100644 --- a/components/stats.js +++ b/components/stats.js @@ -9,16 +9,17 @@ import cycleModule from '../lib/cycle' import {getCycleLengthStats as getCycleInfo} from '../lib/cycle-length' import {stats as labels} from './labels' import AppText from './app-text' +import { getCycleStartsSortedByDate } from '../db' export default class Stats extends Component { render() { - const allMensesStarts = cycleModule().getAllMensesStarts() + const allMensesStarts = getCycleStartsSortedByDate() const atLeastOneCycle = allMensesStarts.length > 1 let cycleLengths let numberOfCycles let cycleInfo if (atLeastOneCycle) { - cycleLengths = cycleModule().getCycleLength(allMensesStarts) + cycleLengths = cycleModule().getAllCycleLengths() numberOfCycles = cycleLengths.length if (numberOfCycles > 1) { cycleInfo = getCycleInfo(cycleLengths) diff --git a/db/index.js b/db/index.js index d060896..e1e180e 100644 --- a/db/index.js +++ b/db/index.js @@ -51,6 +51,10 @@ export function getCycleDaysSortedByDate() { return db.objects('CycleDay').sorted('date', true) } +export function getCycleStartsSortedByDate() { + return db.objects('CycleDay').filtered('isCycleStart = true').sorted('date', true) +} + export function saveSymptom(symptom, cycleDay, val) { db.write(() => { if (symptom === 'bleeding') { diff --git a/lib/cycle.js b/lib/cycle.js index f565ed5..872560d 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -5,6 +5,7 @@ const DAYS = joda.ChronoUnit.DAYS export default function config(opts) { let bleedingDaysSortedByDate + let cycleStartsSortedByDate let cycleDaysSortedByDate let maxBreakInBleeding let maxCycleLength @@ -14,57 +15,22 @@ export default function config(opts) { // we only want to require (and run) the db module // when not running the tests bleedingDaysSortedByDate = require('../db').getBleedingDaysSortedByDate() + cycleStartsSortedByDate = require('../db').getCycleStartsSortedByDate() cycleDaysSortedByDate = require('../db').getCycleDaysSortedByDate() maxBreakInBleeding = 1 maxCycleLength = 99 minCyclesForPrediction = 3 } else { bleedingDaysSortedByDate = opts.bleedingDaysSortedByDate || [] + cycleStartsSortedByDate = opts.cycleStartsSortedByDate || [] cycleDaysSortedByDate = opts.cycleDaysSortedByDate || [] maxBreakInBleeding = opts.maxBreakInBleeding || 1 maxCycleLength = opts.maxCycleLength || 99 minCyclesForPrediction = opts.minCyclesForPrediction || 3 } - function findLatestMensesStart(bleedingDays) { - if (!bleedingDays.length) return null - - // assumes bleeding days are ordered latest first, and - // excluded values already removed - const lastMensesStart = bleedingDays.find((day, i) => { - return noBleedingDayWithinThreshold(day, bleedingDays.slice(i + 1)) - }) - - function noBleedingDayWithinThreshold(day, previousBleedingDays) { - const localDate = LocalDate.parse(day.date) - const threshold = localDate.minusDays(maxBreakInBleeding + 1).toString() - return !previousBleedingDays.some(({ date }) => date >= threshold) - } - - return lastMensesStart - } - function getLastMensesStartForDay(targetDateString) { - // the index of the first bleeding day before the target day - const index = bleedingDaysSortedByDate.findIndex(day => { - return day.date <= targetDateString && !day.bleeding.exclude - }) - - if (index < 0) return null - - const prevBleedingDays = bleedingDaysSortedByDate.slice(index) - return findLatestMensesStart(prevBleedingDays) - } - - function getFollowingMensesStartForDay(targetDateString) { - const followingBleedingDays = bleedingDaysSortedByDate - .filter(day => !day.bleeding.exclude) - .reverse() - - const firstBleedingDayAfterTargetDay = followingBleedingDays - .find(day => day.date > targetDateString) - - return firstBleedingDayAfterTargetDay + return cycleStartsSortedByDate.find(start => start.date <= targetDateString) } function getCycleDayNumber(targetDateString) { @@ -78,59 +44,44 @@ export default function config(opts) { return diffInDays + 1 } - function getCyclesBefore(targetCycleStartDay) { - return collectPreviousCycles([], targetCycleStartDay.date) - } - - function collectPreviousCycles(acc, startOfFollowingCycle) { - const cycle = getPreviousCycle(startOfFollowingCycle) - if (!cycle || !cycle.length) return acc - acc.push(cycle) - return collectPreviousCycles(acc, cycle[cycle.length - 1].date) - } - function getPreviousCycle(dateString) { - const startOfCycle = getLastMensesStartForDay(dateString) - if (!startOfCycle) return null - const dateBeforeStartOfCycle = LocalDate - .parse(startOfCycle.date) - .minusDays(1) - .toString() - - return getCycleForDay(dateBeforeStartOfCycle) - } - - function getCycleForDay(dayOrDate) { - const dateString = typeof dayOrDate === 'string' ? dayOrDate : dayOrDate.date const cycleStart = getLastMensesStartForDay(dateString) if (!cycleStart) return null - const cycleStartIndex = cycleDaysSortedByDate.indexOf(cycleStart) - const nextMensesStart = getFollowingMensesStartForDay(dateString) + const i = cycleDaysSortedByDate.indexOf(cycleStart) + const earlierCycleStart = cycleDaysSortedByDate[i - 1] + if (!earlierCycleStart) return null + return getCycleForStartDay(earlierCycleStart) + } + + function getCyclesBefore(targetCycleStartDay) { + const startFromHere = cycleStartsSortedByDate.findIndex(start => { + return start.date < targetCycleStartDay.date + }) + if (startFromHere < 0) return null + return cycleStartsSortedByDate + .slice(startFromHere) + .map(getCycleForStartDay) + } + + function getCycleForStartDay(startDay) { + const cycleStartIndex = cycleDaysSortedByDate.indexOf(startDay) + const i = cycleStartsSortedByDate.indexOf(startDay) + const nextMensesStart = cycleStartsSortedByDate[i - 1] if (nextMensesStart) { return cycleDaysSortedByDate.slice( cycleDaysSortedByDate.indexOf(nextMensesStart) + 1, - cycleStartIndex + 1 + cycleStartIndex + 1, ) } else { return cycleDaysSortedByDate.slice(0, cycleStartIndex + 1) } } - function getAllMensesStarts(initialBleedingDays = bleedingDaysSortedByDate) { - return recurse(initialBleedingDays.filter(d => !d.bleeding.exclude)) - - function recurse(bleedingDays, collectedDates) { - collectedDates = collectedDates || [] - const lastStart = findLatestMensesStart(bleedingDays) - if (!lastStart) { - return collectedDates - } else { - collectedDates.push(lastStart.date) - const index = bleedingDays.indexOf(lastStart) - const remainingDays = bleedingDays.slice(index + 1) - return recurse(remainingDays, collectedDates) - } - } + function getCycleForDay(dayOrDate) { + const dateString = typeof dayOrDate === 'string' ? dayOrDate : dayOrDate.date + const cycleStart = getLastMensesStartForDay(dateString) + if (!cycleStart) return null + return getCycleForStartDay(cycleStart) } function isMensesStart(cycleDay) { @@ -174,32 +125,33 @@ export default function config(opts) { } } - 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 getAllCycleLengths() { + return cycleStartsSortedByDate + .map(day => LocalDate.parse(day.date)) + .reduce((lengths, cycleStart, i, startsAsLocalDates) => { + if (i === startsAsLocalDates.length - 1) return lengths + const prevCycleStart = startsAsLocalDates[i + 1] + const cycleLength = prevCycleStart.until(cycleStart, DAYS) + if (cycleLength <= maxCycleLength) { lengths.push(cycleLength) } + return lengths + }, []) } function getPredictedMenses() { - const allMensesStarts = getAllMensesStarts() + const allMensesStarts = cycleStartsSortedByDate const atLeastOneCycle = allMensesStarts.length > 1 if (!atLeastOneCycle || allMensesStarts.length < minCyclesForPrediction ) { return [] } - const cycleLengths = getCycleLength(allMensesStarts) + const cycleLengths = getAllCycleLengths() const cycleInfo = getCycleLengthStats(cycleLengths) const periodDistance = Math.round(cycleInfo.mean) let periodStartVariation if (cycleInfo.stdDeviation === null) { periodStartVariation = 2 - } else if (cycleInfo.stdDeviation < 1.5) { // threshold is choosen a little arbitrarily + } else if (cycleInfo.stdDeviation < 1.5) { // threshold is chosen a little arbitrarily periodStartVariation = 1 } else { periodStartVariation = 2 @@ -207,7 +159,7 @@ export default function config(opts) { if (periodDistance - 5 < periodStartVariation) { // otherwise predictions overlap return [] } - let lastStart = LocalDate.parse(allMensesStarts[0]) + let lastStart = LocalDate.parse(allMensesStarts[0].date) const predictedMenses = [] for (let i = 0; i < 3; i++) { lastStart = lastStart.plusDays(periodDistance) @@ -227,8 +179,7 @@ export default function config(opts) { getCycleForDay, getPreviousCycle, getCyclesBefore, - getAllMensesStarts, - getCycleLength, + getAllCycleLengths, getPredictedMenses, isMensesStart, getMensesDaysAfter diff --git a/test/cycle.spec.js b/test/cycle.spec.js index d51392a..6af3208 100644 --- a/test/cycle.spec.js +++ b/test/cycle.spec.js @@ -1,158 +1,80 @@ import chai from 'chai' import dirtyChai from 'dirty-chai' import cycleModule from '../lib/cycle' -import { LocalDate } from 'js-joda' const expect = chai.expect chai.use(dirtyChai) -function useBleedingDays(days) { - return cycleModule({ bleedingDaysSortedByDate: days }).getCycleDayNumber -} +// TODO getPreviousCycle -describe('getCycleDay', () => { +describe('getCycleDayNumber', () => { it('works for a simple example', () => { - const bleedingDays = [{ - date: '2018-05-10', - bleeding: { - value: 2 - } - }, { + const cycleStarts = [{ date: '2018-05-09', + isCycleStart: true, bleeding: { value: 2 } }, { date: '2018-05-03', - bleeding: { - value: 2 - } + isCycleStart: true, + bleeding: { value: 2 } }] - const getCycleDayNumber = useBleedingDays(bleedingDays) + const getCycleDayNumber = cycleModule({ + cycleStartsSortedByDate: cycleStarts + }).getCycleDayNumber const targetDate = '2018-05-17' const result = getCycleDayNumber(targetDate) expect(result).to.eql(9) }) - it('works if some bleedings are exluded', function () { - const bleedingDays = [{ - date: '2018-05-10', - bleeding: { - value: 2, - exclude: true - } - }, { - date: '2018-05-09', - bleeding: { - value: 2, - exclude: true - } - }, { - date: '2018-05-03', - bleeding: { - value: 2 - } - }] - const targetDate = '2018-05-17' - const getCycleDayNumber = useBleedingDays(bleedingDays) - const result = getCycleDayNumber(targetDate) - expect(result).to.eql(15) - }) - it('gets the correct number if the target day is not in the current cycle', () => { - const bleedingDays = [{ + const cycleStarts = [{ date: '2018-05-13', - bleeding: { - value: 2 - } - }, { - date: '2018-04-11', + isCycleStart: true, bleeding: { value: 2 } }, { date: '2018-04-10', - bleeding: { - value: 2 - } + isCycleStart: true, + bleeding: { value: 2 } }] const targetDate = '2018-04-27' - const getCycleDayNumber = useBleedingDays(bleedingDays) + const getCycleDayNumber = cycleModule({ + cycleStartsSortedByDate: cycleStarts + }).getCycleDayNumber const result = getCycleDayNumber(targetDate) expect(result).to.eql(18) }) it('gets the correct number if the target day is the only bleeding day', () => { - const bleedingDays = [{ + const cycleStarts = [{ date: '2018-05-13', - bleeding: { - value: 2 - } + isCycleStart: true, + bleeding: { value: 2 } }] const targetDate = '2018-05-13' - const getCycleDayNumber = useBleedingDays(bleedingDays) + const getCycleDayNumber = cycleModule({ + cycleStartsSortedByDate: cycleStarts + }).getCycleDayNumber const result = getCycleDayNumber(targetDate) expect(result).to.eql(1) }) - describe('getCycleDay returns null', () => { - it('if there are no bleeding days', function () { - const bleedingDays = [] - const targetDate = '2018-05-17' - const getCycleDayNumber = useBleedingDays(bleedingDays) - const result = getCycleDayNumber(targetDate) - expect(result).to.be.null() - }) + it('returns null if there are no bleeding days', function () { + const cycleStarts = [] + const targetDate = '2018-05-17' + const getCycleDayNumber = cycleModule({ + cycleStartsSortedByDate: cycleStarts + }).getCycleDayNumber + const result = getCycleDayNumber(targetDate) + expect(result).to.be.null() }) - describe('getCycleDay with cycle thresholds', () => { - const maxBreakInBleeding = 3 - it('disregards bleeding breaks shorter than max allowed bleeding break in a bleeding period', () => { - const bleedingDays = [{ - date: '2018-05-14', - bleeding: { - value: 2 - } - }, { - date: '2018-05-10', - bleeding: { - value: 2 - } - }] - - const targetDate = '2018-05-17' - const getCycleDayNumber = cycleModule({ - bleedingDaysSortedByDate: bleedingDays, - maxBreakInBleeding - }).getCycleDayNumber - const result = getCycleDayNumber(targetDate) - expect(result).to.eql(8) - }) - - it('counts bleeding breaks longer than maxAllowedBleedingBreak in a bleeding period', () => { - const bleedingDays = [{ - date: '2018-05-14', - bleeding: { - value: 2 - } - }, { - date: '2018-05-09', - bleeding: { - value: 2 - } - }] - const targetDate = '2018-05-17' - const getCycleDayNumber = cycleModule({ - bleedingDaysSortedByDate: bleedingDays, - maxBreakInBleeding - }).getCycleDayNumber - const result = getCycleDayNumber(targetDate) - expect(result).to.eql(4) - }) - }) }) describe('getCyclesBefore', () => { @@ -196,9 +118,18 @@ describe('getCyclesBefore', () => { }, ] + const cycleStarts = [ + '2018-07-05', + '2018-06-05', + '2018-05-03', + '2018-04-02' + ] + const { getCyclesBefore } = cycleModule({ cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => { + return cycleStarts.includes(d.date) + }) }) const result = getCyclesBefore(cycleDaysSortedByDate[0]) expect(result.length).to.eql(3) @@ -282,9 +213,18 @@ describe('getCycleForDay', () => { bleeding: { value: 2 } }, ] + const cycleStarts = [ + '2018-07-05', + '2018-06-05', + '2018-05-03', + '2018-04-02' + ] + const { getCycleForDay } = cycleModule({ cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => { + return cycleStarts.includes(d.date) + }) }) it('gets cycle that has only one day', () => { @@ -350,16 +290,21 @@ describe('getPredictedMenses', () => { describe('cannot predict next menses', () => { it('if no bleeding is documented', () => { const cycleDaysSortedByDate = [ {} ] + const cycleStarts = [] const { getPredictedMenses } = cycleModule({ cycleDaysSortedByDate, bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding), + cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => { + return cycleStarts.includes(d.date) + }), maxCycleLength: 99, minCyclesForPrediction: 1 }) const result = getPredictedMenses() expect(result).to.eql([]) }) + it('if one bleeding is documented (no completed cycle)', () => { const cycleDaysSortedByDate = [ { @@ -367,16 +312,21 @@ describe('getPredictedMenses', () => { bleeding: { value: 2 } } ] + const cycleStarts = ['2018-06-02'] const { getPredictedMenses } = cycleModule({ cycleDaysSortedByDate, bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding), + cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => { + return cycleStarts.includes(d.date) + }), maxCycleLength: 99, minCyclesForPrediction: 1 }) const result = getPredictedMenses() expect(result).to.eql([]) }) + it('if number of cycles is below minCyclesForPrediction', () => { const cycleDaysSortedByDate = [ { @@ -392,10 +342,14 @@ describe('getPredictedMenses', () => { bleeding: { value: 2 } }, ] + const cycleStarts = ['2018-06-01', '2018-05-01'] const { getPredictedMenses } = cycleModule({ cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding), + cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => { + return cycleStarts.includes(d.date) + }), }) const result = getPredictedMenses() expect(result).to.eql([]) @@ -413,10 +367,12 @@ describe('getPredictedMenses', () => { bleeding: { value: 2 } } ] - + const cycleStarts = ['2018-07-15', '2018-07-01'] const { getPredictedMenses } = cycleModule({ cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding), + cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => { + return cycleStarts.includes(d.date) + }), minCyclesForPrediction: 1 }) const result = getPredictedMenses() @@ -445,6 +401,7 @@ describe('getPredictedMenses', () => { ] expect(result).to.eql(expectedResult) }) + it('if number of cycles is above minCyclesForPrediction', () => { const cycleDaysSortedByDate = [ { @@ -467,7 +424,8 @@ describe('getPredictedMenses', () => { const { getPredictedMenses } = cycleModule({ cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + cycleStartsSortedByDate: cycleDaysSortedByDate, + minCyclesForPrediction: 1 }) const result = getPredictedMenses() const expectedResult = [ @@ -489,6 +447,7 @@ describe('getPredictedMenses', () => { ] expect(result).to.eql(expectedResult) }) + it('3 cycles with little standard deviation', () => { const cycleDaysSortedByDate = [ { @@ -511,7 +470,7 @@ describe('getPredictedMenses', () => { const { getPredictedMenses } = cycleModule({ cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + cycleStartsSortedByDate: cycleDaysSortedByDate }) const result = getPredictedMenses() const expectedResult = [ @@ -533,6 +492,7 @@ describe('getPredictedMenses', () => { ] expect(result).to.eql(expectedResult) }) + it('3 cycles with bigger standard deviation', () => { const cycleDaysSortedByDate = [ { @@ -555,7 +515,7 @@ describe('getPredictedMenses', () => { const { getPredictedMenses } = cycleModule({ cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + cycleStartsSortedByDate: cycleDaysSortedByDate }) const result = getPredictedMenses() const expectedResult = [ @@ -586,107 +546,6 @@ describe('getPredictedMenses', () => { }) }) -describe('getAllMensesStart', () => { - it('works for one cycle start', () => { - const cycleDaysSortedByDate = [ - { - date: '2018-05-01', - bleeding: { value: 1 } - } - ] - - const { getAllMensesStarts } = cycleModule({ - cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) - }) - const result = getAllMensesStarts() - expect(result.length).to.eql(1) - expect(result).to.eql(['2018-05-01']) - }), - it('works for two cycle starts', () => { - const cycleDaysSortedByDate = [ - { - date: '2018-06-02', - bleeding: { value: 2 } - }, - { - date: '2018-06-01', - bleeding: { value: 2 } - }, - { - date: '2018-05-01', - bleeding: { value: 2 } - } - ] - - const { getAllMensesStarts } = cycleModule({ - cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) - }) - const result = getAllMensesStarts() - expect(result.length).to.eql(2) - expect(result).to.eql(['2018-06-01', '2018-05-01']) - }) - - it('works for two cycle starts with excluded data', () => { - const cycleDaysSortedByDate = [ - { - date: '2018-06-01', - bleeding: { value: 2 } - }, - { - date: '2018-05-01', - bleeding: { value: 2 } - }, - { - date: '2018-04-31', - bleeding: { value: 2 , exclude: true} - }, - ] - - const { getAllMensesStarts } = cycleModule({ - cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) - }) - const result = getAllMensesStarts() - expect(result.length).to.eql(2) - expect(result).to.eql(['2018-06-01', '2018-05-01']) - }) - - it('returns an empty array if no bleeding days are given', () => { - const cycleDaysSortedByDate = [ {} ] - - const { getAllMensesStarts } = cycleModule({ - cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) - }) - const result = getAllMensesStarts() - expect(result.length).to.eql(0) - expect(result).to.eql([]) - }) - - it('is not slow with 500 menses starts', () => { - const startDate = LocalDate.parse('2018-10-01') - const cycleDaysSortedByDate = Array(500) - .fill(null) - .map((_, i) => { - return { - date: startDate.minusMonths(i).toString(), - bleeding: { value: 2 } - } - }) - const { getAllMensesStarts } = cycleModule({ - cycleDaysSortedByDate, - bleedingDaysSortedByDate: cycleDaysSortedByDate - }) - const start = Date.now() - const result = getAllMensesStarts() - const duration = Date.now() - start - expect(result.length).to.eql(500) - expect(duration).to.be.lessThan(100) - }) -}) - describe('isMensesStart', () => { it('works for simple menses start', () => { const cycleDaysSortedByDate = [ @@ -827,6 +686,51 @@ describe('isMensesStart', () => { const start = isMensesStart(cycleDaysSortedByDate[2]) expect(start).to.be.true() }) + describe('with cycle thresholds', () => { + const maxBreakInBleeding = 3 + + it('disregards bleeding breaks equal to max allowed bleeding break in a bleeding period', () => { + const bleedingDays = [{ + date: '2018-05-14', + bleeding: { + value: 2 + } + }, { + date: '2018-05-10', + bleeding: { + value: 2 + } + }] + + const isMensesStart = cycleModule({ + bleedingDaysSortedByDate: bleedingDays, + maxBreakInBleeding + }).isMensesStart + const result = isMensesStart(bleedingDays[0]) + expect(result).to.be.false() + }) + + it('counts bleeding breaks longer than maxAllowedBleedingBreak in a bleeding period', () => { + const bleedingDays = [{ + date: '2018-05-14', + bleeding: { + value: 2 + } + }, { + date: '2018-05-09', + bleeding: { + value: 2 + } + }] + + const isMensesStart = cycleModule({ + bleedingDaysSortedByDate: bleedingDays, + maxBreakInBleeding + }).isMensesStart + const result = isMensesStart(bleedingDays[0]) + expect(result).to.be.true() + }) + }) }) describe('getMensesDaysAfter', () => { @@ -999,4 +903,49 @@ describe('getMensesDaysAfter', () => { } ]) }) + describe('with cycle thresholds', () => { + const maxBreakInBleeding = 3 + + it('disregards bleeding breaks shorter than max allowed bleeding break in a bleeding period', () => { + const bleedingDays = [{ + date: '2018-05-14', + bleeding: { + value: 2 + } + }, { + date: '2018-05-10', + bleeding: { + value: 2 + } + }] + + const getMensesDaysAfter = cycleModule({ + bleedingDaysSortedByDate: bleedingDays, + maxBreakInBleeding + }).getMensesDaysAfter + const result = getMensesDaysAfter(bleedingDays[1]) + expect(result).to.eql([bleedingDays[0]]) + }) + + it('counts bleeding breaks longer than maxAllowedBleedingBreak in a bleeding period', () => { + const bleedingDays = [{ + date: '2018-05-14', + bleeding: { + value: 2 + } + }, { + date: '2018-05-09', + bleeding: { + value: 2 + } + }] + + const getMensesDaysAfter = cycleModule({ + bleedingDaysSortedByDate: bleedingDays, + maxBreakInBleeding + }).getMensesDaysAfter + const result = getMensesDaysAfter(bleedingDays[1]) + expect(result).to.eql([]) + }) + }) }) \ No newline at end of file From 5e8bc0a3e8453daf09b06ad7a11d535b0ba4d180 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 25 Oct 2018 09:30:44 +0200 Subject: [PATCH 05/12] Add test for getPreviousCycle --- lib/cycle.js | 4 +- test/cycle.spec.js | 175 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 175 insertions(+), 4 deletions(-) diff --git a/lib/cycle.js b/lib/cycle.js index 872560d..dcac869 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -47,8 +47,8 @@ export default function config(opts) { function getPreviousCycle(dateString) { const cycleStart = getLastMensesStartForDay(dateString) if (!cycleStart) return null - const i = cycleDaysSortedByDate.indexOf(cycleStart) - const earlierCycleStart = cycleDaysSortedByDate[i - 1] + const i = cycleStartsSortedByDate.indexOf(cycleStart) + const earlierCycleStart = cycleStartsSortedByDate[i + 1] if (!earlierCycleStart) return null return getCycleForStartDay(earlierCycleStart) } diff --git a/test/cycle.spec.js b/test/cycle.spec.js index 6af3208..8508748 100644 --- a/test/cycle.spec.js +++ b/test/cycle.spec.js @@ -5,8 +5,6 @@ import cycleModule from '../lib/cycle' const expect = chai.expect chai.use(dirtyChai) -// TODO getPreviousCycle - describe('getCycleDayNumber', () => { it('works for a simple example', () => { const cycleStarts = [{ @@ -77,6 +75,179 @@ describe('getCycleDayNumber', () => { }) +describe('getPreviousCycle', () => { + it('gets previous cycle', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-07-05', + bleeding: { value: 2 } + }, + { + date: '2018-06-05', + bleeding: { value: 2 } + }, + { + date: '2018-05-05', + mucus: { value: 2 } + }, + { + date: '2018-05-04', + bleeding: { value: 2 } + }, + { + date: '2018-05-03', + bleeding: { value: 2 } + }, + { + date: '2018-04-05', + mucus: { value: 2 } + }, + { + date: '2018-04-04', + mucus: { value: 2 } + }, + { + date: '2018-04-03', + mucus: { value: 2 } + }, + { + date: '2018-04-02', + bleeding: { value: 2 } + }, + ] + + const cycleStarts = [ + '2018-07-05', + '2018-06-05', + '2018-05-03', + '2018-04-02' + ] + + const { getPreviousCycle } = cycleModule({ + cycleDaysSortedByDate, + cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => { + return cycleStarts.includes(d.date) + }) + }) + const result = getPreviousCycle('2018-06-08') + expect(result).to.eql([ + { + date: '2018-05-05', + mucus: { value: 2 } + }, + { + date: '2018-05-04', + bleeding: { value: 2 } + }, + { + date: '2018-05-03', + bleeding: { value: 2 } + } + ]) + }) + + it('returns null when target days is not in a cyle', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-07-05', + }, + { + date: '2018-06-05', + }, + { + date: '2018-05-05', + }, + { + date: '2018-05-04', + }, + { + date: '2018-05-03', + }, + { + date: '2018-04-05', + }, + { + date: '2018-04-04', + mucus: { value: 2 } + }, + { + date: '2018-04-03', + }, + { + date: '2018-04-02', + }, + ] + + const cycleStarts = [] + + const { getPreviousCycle } = cycleModule({ + cycleDaysSortedByDate, + cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => { + return cycleStarts.includes(d.date) + }) + }) + const result = getPreviousCycle('2018-06-08') + expect(result).to.eql(null) + }) + + it('returns null when there is no previous cycle', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-07-05', + bleeding: { value: 2 } + }, + { + date: '2018-06-05', + bleeding: { value: 2 } + }, + { + date: '2018-05-05', + mucus: { value: 2 } + }, + { + date: '2018-05-04', + bleeding: { value: 2 } + }, + { + date: '2018-05-03', + bleeding: { value: 2 } + }, + { + date: '2018-04-05', + mucus: { value: 2 } + }, + { + date: '2018-04-04', + mucus: { value: 2 } + }, + { + date: '2018-04-03', + mucus: { value: 2 } + }, + { + date: '2018-04-02', + bleeding: { value: 2 } + }, + ] + + const cycleStarts = [ + '2018-07-05', + '2018-06-05', + '2018-05-03', + '2018-04-02' + ] + + const { getPreviousCycle } = cycleModule({ + cycleDaysSortedByDate, + cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => { + return cycleStarts.includes(d.date) + }) + }) + const result = getPreviousCycle('2018-04-18') + expect(result).to.eql(null) + }) +}) + describe('getCyclesBefore', () => { it('gets previous cycles', () => { const cycleDaysSortedByDate = [ From e2b426727b4f2be72ab705a57fdafa66c6a6a6d6 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 25 Oct 2018 10:00:39 +0200 Subject: [PATCH 06/12] Set cycleStart to false when bleeding deleted --- db/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/index.js b/db/index.js index e1e180e..a9322ff 100644 --- a/db/index.js +++ b/db/index.js @@ -65,6 +65,7 @@ export function saveSymptom(symptom, cycleDay, val) { }) } +// TODO this also needs a test export function saveBleeding(cycleDay, bleeding) { if (!bleeding) { updateCycleDayAndMaybeSetNewCycleStart(cycleDay, bleeding) @@ -84,6 +85,7 @@ export function saveBleeding(cycleDay, bleeding) { // then update the cycle day const mensesDaysAfter = getMensesDaysAfter(oldCycleDay) oldCycleDay.bleeding = newValue + oldCycleDay.isCycleStart = false if (!mensesDaysAfter.length) return From de674590e2333c3ca89dcd74c822d7b8aa11bfc4 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 25 Oct 2018 15:06:22 +0200 Subject: [PATCH 07/12] Make getMensesDaysAfter work for non-bleeding day too --- lib/cycle.js | 59 ++++++++++++++++++++++++++++------- test/cycle.spec.js | 78 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 108 insertions(+), 29 deletions(-) diff --git a/lib/cycle.js b/lib/cycle.js index dcac869..a5bd7d3 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -101,21 +101,23 @@ export default function config(opts) { } } - function getMensesDaysAfter(cycleDay) { - const bleedingDays = bleedingDaysSortedByDate.filter(d => { - return !d.bleeding.exclude + function getMensesDaysRightAfter(cycleDay) { + const bleedingDays = bleedingDaysSortedByDate + .filter(d => { + return !d.bleeding.exclude + }) + .reverse() + const firstFollowingBleedingDayIndex = bleedingDays.findIndex(day => { + return day.date > cycleDay.date }) - const startIndex = bleedingDays.findIndex(day => { - return day.date === cycleDay.date - }) - return recurse(cycleDay, startIndex, []) + return recurse(cycleDay, firstFollowingBleedingDayIndex, []) - function recurse(day, i, mensesDays) { - if (i === 0) return mensesDays - const next = bleedingDays[i - 1] + function recurse(day, nextIndex, mensesDays) { + const next = bleedingDays[nextIndex] + if (!next) return mensesDays if (!isWithinThreshold(day, next)) return mensesDays mensesDays.unshift(next) - return recurse(next, i - 1, mensesDays) + return recurse(next, nextIndex + 1, mensesDays) } function isWithinThreshold(cycleDay, nextCycleDay) { @@ -174,6 +176,37 @@ export default function config(opts) { return predictedMenses } + + // TODO this also needs a test + function maybeSetNewCycleStart(oldCycleDay, newValue) { + // if a bleeding value is deleted, we need to check if + // there are any following bleeding days and if the + // next one of them is now a cycle start + + // in order to get the menses days, the cycle day in question still + // has to have a bleeding value, so we get those days first and only + // then update the cycle day + const mensesDaysAfter = getMensesDaysRightAfter(oldCycleDay) + oldCycleDay.bleeding = newValue + oldCycleDay.isCycleStart = false + + if (!mensesDaysAfter.length) return + + const nextOne = mensesDaysAfter[mensesDaysAfter.length - 1] + if (isMensesStart(nextOne)) { + nextOne.isCycleStart = true + } + } + + function maybeClearOldCycleStartsInThisMenses(cycleDay) { + // if we have a new bleeding day, we need to clear the + // menses start marker from all following days of this + // menses that may have been marked as start before + const mensesDaysAfter = getMensesDaysRightAfter(cycleDay) + mensesDaysAfter.forEach(day => day.isCycleStart = false) + } + + return { getCycleDayNumber, getCycleForDay, @@ -182,6 +215,8 @@ export default function config(opts) { getAllCycleLengths, getPredictedMenses, isMensesStart, - getMensesDaysAfter + getMensesDaysRightAfter, + maybeSetNewCycleStart, + maybeClearOldCycleStartsInThisMenses } } \ No newline at end of file diff --git a/test/cycle.spec.js b/test/cycle.spec.js index 8508748..af07e6d 100644 --- a/test/cycle.spec.js +++ b/test/cycle.spec.js @@ -904,7 +904,7 @@ describe('isMensesStart', () => { }) }) -describe('getMensesDaysAfter', () => { +describe('getMensesDaysRightAfter', () => { it('works for simple menses start', () => { const cycleDaysSortedByDate = [ { @@ -927,11 +927,11 @@ describe('getMensesDaysAfter', () => { } ] - const { getMensesDaysAfter } = cycleModule({ + const { getMensesDaysRightAfter } = cycleModule({ cycleDaysSortedByDate, bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) }) - const days = getMensesDaysAfter(cycleDaysSortedByDate[3]) + const days = getMensesDaysRightAfter(cycleDaysSortedByDate[3]) expect(days).to.eql([ { date: '2018-05-03', @@ -944,6 +944,50 @@ describe('getMensesDaysAfter', () => { ]) }) + it('works when the day is not a bleeding day', () => { + const cycleDaysSortedByDate = [ + { + date: '2018-05-04', + }, + { + date: '2018-05-03', + bleeding: { value: 1 } + }, + { + date: '2018-05-02', + bleeding: { value: 1 } + }, + { + date: '2018-05-01', + bleeding: { value: 1 } + }, + { + date: '2018-04-30', + bleeding: null + } + ] + + const { getMensesDaysRightAfter } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const days = getMensesDaysRightAfter(cycleDaysSortedByDate[4]) + expect(days).to.eql([ + { + date: '2018-05-03', + bleeding: { value: 1 } + }, + { + date: '2018-05-02', + bleeding: { value: 1 } + }, + { + date: '2018-05-01', + bleeding: { value: 1 } + } + ]) + }) + it('ignores excluded values', () => { const cycleDaysSortedByDate = [ { @@ -966,11 +1010,11 @@ describe('getMensesDaysAfter', () => { } ] - const { getMensesDaysAfter } = cycleModule({ + const { getMensesDaysRightAfter } = cycleModule({ cycleDaysSortedByDate, bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) }) - const days = getMensesDaysAfter(cycleDaysSortedByDate[3]) + const days = getMensesDaysRightAfter(cycleDaysSortedByDate[3]) expect(days).to.eql([ { date: '2018-05-03', @@ -999,11 +1043,11 @@ describe('getMensesDaysAfter', () => { } ] - const { getMensesDaysAfter } = cycleModule({ + const { getMensesDaysRightAfter } = cycleModule({ cycleDaysSortedByDate, bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) }) - const days = getMensesDaysAfter(cycleDaysSortedByDate[3]) + const days = getMensesDaysRightAfter(cycleDaysSortedByDate[3]) expect(days).to.eql([]) }) @@ -1028,11 +1072,11 @@ describe('getMensesDaysAfter', () => { } ] - const { getMensesDaysAfter } = cycleModule({ + const { getMensesDaysRightAfter } = cycleModule({ cycleDaysSortedByDate, bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) }) - const days = getMensesDaysAfter(cycleDaysSortedByDate[3]) + const days = getMensesDaysRightAfter(cycleDaysSortedByDate[3]) expect(days).to.eql([]) }) @@ -1058,11 +1102,11 @@ describe('getMensesDaysAfter', () => { } ] - const { getMensesDaysAfter } = cycleModule({ + const { getMensesDaysRightAfter } = cycleModule({ cycleDaysSortedByDate, bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) }) - const days = getMensesDaysAfter(cycleDaysSortedByDate[3]) + const days = getMensesDaysRightAfter(cycleDaysSortedByDate[3]) expect(days).to.eql([ { date: '2018-05-05', @@ -1090,11 +1134,11 @@ describe('getMensesDaysAfter', () => { } }] - const getMensesDaysAfter = cycleModule({ + const getMensesDaysRightAfter = cycleModule({ bleedingDaysSortedByDate: bleedingDays, maxBreakInBleeding - }).getMensesDaysAfter - const result = getMensesDaysAfter(bleedingDays[1]) + }).getMensesDaysRightAfter + const result = getMensesDaysRightAfter(bleedingDays[1]) expect(result).to.eql([bleedingDays[0]]) }) @@ -1111,11 +1155,11 @@ describe('getMensesDaysAfter', () => { } }] - const getMensesDaysAfter = cycleModule({ + const getMensesDaysRightAfter = cycleModule({ bleedingDaysSortedByDate: bleedingDays, maxBreakInBleeding - }).getMensesDaysAfter - const result = getMensesDaysAfter(bleedingDays[1]) + }).getMensesDaysRightAfter + const result = getMensesDaysRightAfter(bleedingDays[1]) expect(result).to.eql([]) }) }) From 918d383163ab5c5308550c860953632ed7b4b822 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 25 Oct 2018 15:17:26 +0200 Subject: [PATCH 08/12] Simplify cycle start detection funtions --- db/index.js | 47 ++++++++++++++++++++++------------------------- lib/cycle.js | 34 +--------------------------------- 2 files changed, 23 insertions(+), 58 deletions(-) diff --git a/db/index.js b/db/index.js index a9322ff..99436bb 100644 --- a/db/index.js +++ b/db/index.js @@ -8,7 +8,7 @@ import cycleModule from '../lib/cycle' let db let isMensesStart -let getMensesDaysAfter +let getMensesDaysRightAfter export async function openDb ({ hash, persistConnection }) { const realmConfig = {} @@ -37,7 +37,7 @@ export async function openDb ({ hash, persistConnection }) { if (persistConnection) db = connection const cycle = cycleModule() isMensesStart = cycle.isMensesStart - getMensesDaysAfter = cycle.getMensesDaysAfter + getMensesDaysRightAfter = cycle.getMensesDaysRightAfter } @@ -57,51 +57,48 @@ export function getCycleStartsSortedByDate() { export function saveSymptom(symptom, cycleDay, val) { db.write(() => { - if (symptom === 'bleeding') { - saveBleeding(cycleDay, val) + if (bleedingValueDeleted(symptom, val)) { + cycleDay.bleeding = val + cycleDay.isCycleStart = false + maybeSetNewCycleStart(cycleDay, val) + } else if (bleedingValueAddedOrChanged(symptom, val)) { + cycleDay.bleeding = val + cycleDay.isCycleStart = isMensesStart(cycleDay) + maybeClearOldCycleStarts(cycleDay) } else { cycleDay[symptom] = val } }) -} -// TODO this also needs a test -export function saveBleeding(cycleDay, bleeding) { - if (!bleeding) { - updateCycleDayAndMaybeSetNewCycleStart(cycleDay, bleeding) - } else { - cycleDay.bleeding = bleeding - cycleDay.isCycleStart = isMensesStart(cycleDay) - maybeClearOldCycleStartsInThisMenses(cycleDay) + function bleedingValueDeleted(symptom, val) { + return symptom === 'bleeding' && !val } - function updateCycleDayAndMaybeSetNewCycleStart(oldCycleDay, newValue) { + function bleedingValueAddedOrChanged(symptom, val) { + return symptom === 'bleeding' && val + } + + function maybeSetNewCycleStart(dayWithDeletedBleeding) { // if a bleeding value is deleted, we need to check if // there are any following bleeding days and if the // next one of them is now a cycle start - - // in order to get the menses days, the cycle day in question still - // has to have a bleeding value, so we get those days first and only - // then update the cycle day - const mensesDaysAfter = getMensesDaysAfter(oldCycleDay) - oldCycleDay.bleeding = newValue - oldCycleDay.isCycleStart = false - + const mensesDaysAfter = getMensesDaysRightAfter(dayWithDeletedBleeding) if (!mensesDaysAfter.length) return - const nextOne = mensesDaysAfter[mensesDaysAfter.length - 1] if (isMensesStart(nextOne)) { nextOne.isCycleStart = true } } - function maybeClearOldCycleStartsInThisMenses(cycleDay) { + function maybeClearOldCycleStarts(cycleDay) { // if we have a new bleeding day, we need to clear the // menses start marker from all following days of this // menses that may have been marked as start before - const mensesDaysAfter = getMensesDaysAfter(cycleDay) + const mensesDaysAfter = getMensesDaysRightAfter(cycleDay) mensesDaysAfter.forEach(day => day.isCycleStart = false) } + + } export function getOrCreateCycleDay(localDate) { diff --git a/lib/cycle.js b/lib/cycle.js index a5bd7d3..1be36ab 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -177,36 +177,6 @@ export default function config(opts) { } - // TODO this also needs a test - function maybeSetNewCycleStart(oldCycleDay, newValue) { - // if a bleeding value is deleted, we need to check if - // there are any following bleeding days and if the - // next one of them is now a cycle start - - // in order to get the menses days, the cycle day in question still - // has to have a bleeding value, so we get those days first and only - // then update the cycle day - const mensesDaysAfter = getMensesDaysRightAfter(oldCycleDay) - oldCycleDay.bleeding = newValue - oldCycleDay.isCycleStart = false - - if (!mensesDaysAfter.length) return - - const nextOne = mensesDaysAfter[mensesDaysAfter.length - 1] - if (isMensesStart(nextOne)) { - nextOne.isCycleStart = true - } - } - - function maybeClearOldCycleStartsInThisMenses(cycleDay) { - // if we have a new bleeding day, we need to clear the - // menses start marker from all following days of this - // menses that may have been marked as start before - const mensesDaysAfter = getMensesDaysRightAfter(cycleDay) - mensesDaysAfter.forEach(day => day.isCycleStart = false) - } - - return { getCycleDayNumber, getCycleForDay, @@ -215,8 +185,6 @@ export default function config(opts) { getAllCycleLengths, getPredictedMenses, isMensesStart, - getMensesDaysRightAfter, - maybeSetNewCycleStart, - maybeClearOldCycleStartsInThisMenses + getMensesDaysRightAfter } } \ No newline at end of file From 8445cc8ed831caa3146f01349f7bf16c5b1dd653 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 25 Oct 2018 15:42:10 +0200 Subject: [PATCH 09/12] Determine isCycleStart on importing data too --- db/index.js | 19 ++++++++++++++----- lib/import-export/import-from-csv.js | 8 +++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/db/index.js b/db/index.js index 99436bb..fe8c269 100644 --- a/db/index.js +++ b/db/index.js @@ -40,7 +40,6 @@ export async function openDb ({ hash, persistConnection }) { getMensesDaysRightAfter = cycle.getMensesDaysRightAfter } - export function getBleedingDaysSortedByDate() { return db.objects('CycleDay').filtered('bleeding != null').sorted('date', true) } @@ -97,8 +96,16 @@ export function saveSymptom(symptom, cycleDay, val) { const mensesDaysAfter = getMensesDaysRightAfter(cycleDay) mensesDaysAfter.forEach(day => day.isCycleStart = false) } +} - +export function updateCycleStartsForAllCycleDays() { + db.write(() => { + getBleedingDaysSortedByDate().forEach(day => { + if (isMensesStart(day)) { + day.isCycleStart = true + } + }) + }) } export function getOrCreateCycleDay(localDate) { @@ -128,8 +135,10 @@ export function getPreviousTemperature(cycleDay) { return winner.temperature.value } -export function tryToCreateCycleDay(day, i) { +function tryToCreateCycleDayFromImport(day, i) { try { + // we cannot know this yet, gets detected afterwards + day.isCycleStart = false db.create('CycleDay', day) } catch (err) { const msg = `Line ${i + 1}(${day.date}): ${err.message}` @@ -157,7 +166,7 @@ export function getSchema() { export function tryToImportWithDelete(cycleDays) { db.write(() => { db.delete(db.objects('CycleDay')) - cycleDays.forEach(tryToCreateCycleDay) + cycleDays.forEach(tryToCreateCycleDayFromImport) }) } @@ -166,7 +175,7 @@ export function tryToImportWithoutDelete(cycleDays) { cycleDays.forEach((day, i) => { const existing = getCycleDay(day.date) if (existing) db.delete(existing) - tryToCreateCycleDay(day, i) + tryToCreateCycleDayFromImport(day, i) }) }) } diff --git a/lib/import-export/import-from-csv.js b/lib/import-export/import-from-csv.js index c34ebc9..4731eea 100644 --- a/lib/import-export/import-from-csv.js +++ b/lib/import-export/import-from-csv.js @@ -1,6 +1,11 @@ import csvParser from 'csvtojson' import isObject from 'isobject' -import { getSchema, tryToImportWithDelete, tryToImportWithoutDelete } from '../../db' +import { + getSchema, + tryToImportWithDelete, + tryToImportWithoutDelete, + updateCycleStartsForAllCycleDays +} from '../../db' import getColumnNamesForCsv from './get-csv-column-names' export default async function importCsv(csv, deleteFirst) { @@ -49,6 +54,7 @@ export default async function importCsv(csv, deleteFirst) { } else { tryToImportWithoutDelete(cycleDays) } + updateCycleStartsForAllCycleDays() } function validateHeaders(headers) { From d2837ab5ae8be63abd6cec0820ef01eada2a89f4 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 25 Oct 2018 16:02:21 +0200 Subject: [PATCH 10/12] Exclude isCycleStart in export --- lib/import-export/get-csv-column-names.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/import-export/get-csv-column-names.js b/lib/import-export/get-csv-column-names.js index 3e8b425..d8626cd 100644 --- a/lib/import-export/get-csv-column-names.js +++ b/lib/import-export/get-csv-column-names.js @@ -7,6 +7,9 @@ export default function getColumnNamesForCsv() { const schema = getSchema() const model = schema[schemaName] return Object.keys(model).reduce((acc, key) => { + // we don't want to include isCycleStart, because that is + // a derived value + if (key === 'isCycleStart') return acc const prefixedKey = prefix ? [prefix, key].join('.') : key const childSchemaName = model[key].objectType if (!childSchemaName) { From eefe3ae1135e983f58f1492ea6e8dcacfa63a702 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 25 Oct 2018 17:28:54 +0200 Subject: [PATCH 11/12] Rename getCycleForCycleStartDay --- lib/cycle.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/cycle.js b/lib/cycle.js index 1be36ab..5a01ae8 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -50,7 +50,7 @@ export default function config(opts) { const i = cycleStartsSortedByDate.indexOf(cycleStart) const earlierCycleStart = cycleStartsSortedByDate[i + 1] if (!earlierCycleStart) return null - return getCycleForStartDay(earlierCycleStart) + return getCycleForCycleStartDay(earlierCycleStart) } function getCyclesBefore(targetCycleStartDay) { @@ -60,10 +60,10 @@ export default function config(opts) { if (startFromHere < 0) return null return cycleStartsSortedByDate .slice(startFromHere) - .map(getCycleForStartDay) + .map(getCycleForCycleStartDay) } - function getCycleForStartDay(startDay) { + function getCycleForCycleStartDay(startDay) { const cycleStartIndex = cycleDaysSortedByDate.indexOf(startDay) const i = cycleStartsSortedByDate.indexOf(startDay) const nextMensesStart = cycleStartsSortedByDate[i - 1] @@ -81,7 +81,7 @@ export default function config(opts) { const dateString = typeof dayOrDate === 'string' ? dayOrDate : dayOrDate.date const cycleStart = getLastMensesStartForDay(dateString) if (!cycleStart) return null - return getCycleForStartDay(cycleStart) + return getCycleForCycleStartDay(cycleStart) } function isMensesStart(cycleDay) { @@ -103,9 +103,7 @@ export default function config(opts) { function getMensesDaysRightAfter(cycleDay) { const bleedingDays = bleedingDaysSortedByDate - .filter(d => { - return !d.bleeding.exclude - }) + .filter(d => !d.bleeding.exclude) .reverse() const firstFollowingBleedingDayIndex = bleedingDays.findIndex(day => { return day.date > cycleDay.date From 3b09d93a4753f84490e2efd283524390ac3bfb15 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 1 Nov 2018 14:50:58 +0100 Subject: [PATCH 12/12] Add comments and rename things for clarity --- lib/cycle.js | 15 ++++++++++++--- test/cycle.spec.js | 6 +++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/cycle.js b/lib/cycle.js index 5a01ae8..0f47690 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -89,6 +89,8 @@ export default function config(opts) { if (noBleedingDayWithinThresholdBefore(cycleDay)) return true return false + // checks that there are no relevant bleeding days before + // the input cycleDay (returns boolean) function noBleedingDayWithinThresholdBefore(cycleDay) { const localDate = LocalDate.parse(cycleDay.date) const threshold = localDate.minusDays(maxBreakInBleeding + 1).toString() @@ -101,6 +103,9 @@ export default function config(opts) { } } + // returns all bleeding days that belong to one menses directly following + // the cycle day. used to set or clear new cycle starts when the target day + // changes function getMensesDaysRightAfter(cycleDay) { const bleedingDays = bleedingDaysSortedByDate .filter(d => !d.bleeding.exclude) @@ -110,6 +115,9 @@ export default function config(opts) { }) return recurse(cycleDay, firstFollowingBleedingDayIndex, []) + // we look at the current bleeding day as well as the next, and decide + // whether they belong to one menses. if they do, we collect them, once + // they don't, we're done function recurse(day, nextIndex, mensesDays) { const next = bleedingDays[nextIndex] if (!next) return mensesDays @@ -118,10 +126,11 @@ export default function config(opts) { return recurse(next, nextIndex + 1, mensesDays) } - function isWithinThreshold(cycleDay, nextCycleDay) { - const localDate = LocalDate.parse(cycleDay.date) + // checks whether the two days belong to one menses episode + function isWithinThreshold(bleedingDay, nextBleedingDay) { + const localDate = LocalDate.parse(bleedingDay.date) const threshold = localDate.plusDays(maxBreakInBleeding + 1).toString() - return nextCycleDay.date <= threshold + return nextBleedingDay.date <= threshold } } diff --git a/test/cycle.spec.js b/test/cycle.spec.js index af07e6d..77bf379 100644 --- a/test/cycle.spec.js +++ b/test/cycle.spec.js @@ -146,7 +146,7 @@ describe('getPreviousCycle', () => { ]) }) - it('returns null when target days is not in a cyle', () => { + it('returns null when target day is not in a cyle', () => { const cycleDaysSortedByDate = [ { date: '2018-07-05', @@ -860,7 +860,7 @@ describe('isMensesStart', () => { describe('with cycle thresholds', () => { const maxBreakInBleeding = 3 - it('disregards bleeding breaks equal to max allowed bleeding break in a bleeding period', () => { + it('disregards bleeding breaks equal to maxAllowedBleedingBreak in a bleeding period', () => { const bleedingDays = [{ date: '2018-05-14', bleeding: { @@ -1121,7 +1121,7 @@ describe('getMensesDaysRightAfter', () => { describe('with cycle thresholds', () => { const maxBreakInBleeding = 3 - it('disregards bleeding breaks shorter than max allowed bleeding break in a bleeding period', () => { + it('disregards bleeding breaks shorter than maxAllowedBleedingBreak in a bleeding period', () => { const bleedingDays = [{ date: '2018-05-14', bleeding: {