diff --git a/lib/sympto/cervix.js b/lib/sympto/cervix.js index 9a59645..2578381 100644 --- a/lib/sympto/cervix.js +++ b/lib/sympto/cervix.js @@ -1,15 +1,26 @@ export default function (cycleDays, tempEvalEndIndex) { - const cervixDays = cycleDays.filter(day => day.cervix && !day.cervix.exclude) + const notDetected = { detected: false } + const cervixDays = cycleDays + .filter(day => day.cervix && !day.cervix.exclude) + .filter(day => typeof day.cervix.opening === 'number' && typeof day.cervix.firmness === 'number') + + // we search for the day of cervix peak, which must: + // * have fertile cervix values + // * be followed by at least 3 days + // these 3 following days must all show infertile cervix values + // if everything applies we must check the days until the end of temperature evaluation + // during these relevantDays no fertile cervix must occur for (let i = 0; i < cervixDays.length; i++) { const day = cervixDays[i] if (isClosedAndHard(day.cervix)) continue - // the three following days must be with closed and hard cervix - // AND no other cervix value may occur until temperature evaluation has - // been completed + + // the three following days must be with closed and hard cervix (indicating an infertile cervix) const threeFollowingDays = cervixDays.slice(i + 1, i + 4) if (threeFollowingDays.length < 3) continue + // no other fertile cervix value may occur until temperature evaluation has + // been completed const fertileCervixOccursIn3FollowingDays = threeFollowingDays.some(day => { return !isClosedAndHard(day.cervix) }) @@ -33,9 +44,9 @@ export default function (cycleDays, tempEvalEndIndex) { } } - return { detected: false } + return notDetected } -function isClosedAndHard (cervix) { - return cervix.value.opening === 0 && cervix.value.firmness === 0 +function isClosedAndHard (cervixDay) { + return cervixDay.opening === 0 && cervixDay.firmness === 0 } diff --git a/lib/sympto/index.js b/lib/sympto/index.js index d73d635..575b608 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -115,10 +115,10 @@ function throwIfArgsAreNotInRequiredFormat(cycles) { if (day.mucus) assert.equal(typeof day.mucus.value, 'number', "Mucus value must be a number.") if (day.mucus) assert.ok(day.mucus.value >= 0, "Mucus value must greater or equal to 0.") if (day.mucus) assert.ok(day.mucus.value <= 4, "Mucus value must be below 5.") - if (day.cervix) assert.ok(day.cervix.value.opening >= 0, "Cervix opening value must be 0 or bigger") - if (day.cervix) assert.ok(day.cervix.value.opening <= 2, "Cervix opening value must be 2 or smaller") - if (day.cervix) assert.ok(day.cervix.value.firmness >= 0, "Cervix firmness value must be 0 or bigger") - if (day.cervix) assert.ok(day.cervix.value.firmness <= 1, "Cervix firmness value must be 1 or smaller") + if (day.cervix) assert.ok(day.cervix.opening >= 0, "Cervix opening value must be 0 or bigger") + if (day.cervix) assert.ok(day.cervix.opening <= 2, "Cervix opening value must be 2 or smaller") + if (day.cervix) assert.ok(day.cervix.firmness >= 0, "Cervix firmness value must be 0 or bigger") + if (day.cervix) assert.ok(day.cervix.firmness <= 1, "Cervix firmness value must be 1 or smaller") assert.equal(typeof cycle[0].bleeding.value, 'number', "Bleeding value must be a number") }) }) diff --git a/test/sympto/cervix-temp-fixtures.js b/test/sympto/cervix-temp-fixtures.js index 0299524..58ff695 100644 --- a/test/sympto/cervix-temp-fixtures.js +++ b/test/sympto/cervix-temp-fixtures.js @@ -4,8 +4,10 @@ function convertToSymptoFormat(val) { value: val.temperature, exclude: false } - if (val.cervix) sympto.cervix = { - value: val.cervix, + + if (val.cervix && typeof val.cervix.opening === 'number' && typeof val.cervix.firmness === 'number') sympto.cervix = { + opening: val.cervix.opening, + firmness: val.cervix.firmness, exclude: false } if (val.bleeding) sympto.bleeding = { @@ -130,7 +132,7 @@ export const tempShift3DaysAfterCervixShift = [ { date: '2018-05-20', temperature: 36.7, cervix: { opening: 0, firmness: 0 } }, { date: '2018-05-21', temperature: 36.6, cervix: { opening: 0, firmness: 0 } }, { date: '2018-05-22', temperature: 36.85, cervix: { opening: 0, firmness: 0 } }, - { date: '2018-05-23', temperature: 36.8, cervix: { opening: 0, firmness: 0 } }, + { date: '2018-05-23', temperature: 36.8, cervix: { opening: 1, firmness: 0 } }, { date: '2018-05-24', temperature: 36.85, cervix: { opening: 0, firmness: 0 } }, { date: '2018-05-25', temperature: 36.95, cervix: { opening: 0, firmness: 0 } }, { date: '2018-05-26', temperature: 36.85, cervix: { opening: 0, firmness: 1 } }, diff --git a/test/sympto/cervix-temp.spec.js b/test/sympto/cervix-temp.spec.js index 5ccd940..e2d8fa0 100644 --- a/test/sympto/cervix-temp.spec.js +++ b/test/sympto/cervix-temp.spec.js @@ -48,10 +48,9 @@ describe('sympto', () => { } }) }) - it('with temp and cervix shifts detects only peri- and post-ovulatory phases', () => { + it('with temp and cervix shifts at the same day an no previous cycle detects only peri- and post-ovulatory phases', () => { const status = getSensiplanStatus({ cycle: cervixShiftAndFhmOnSameDay, - previousCycle: cycleWithoutFhm, secondarySymptom: 'cervix' }) expect(Object.keys(status.phases).length).to.eql(2) @@ -72,7 +71,7 @@ describe('sympto', () => { }) }) describe('with previous higher temp measurement', () => { - it('with no shifts detects only peri-ovulatory in 5-day long cycle according to 5-day rule', () => { + it('with no shifts in 5-day long cycle detects only peri-ovulatory according to 5-day rule', () => { const status = getSensiplanStatus({ cycle: fiveDayCycle, previousCycle: cervixShiftAndFhmOnSameDay, @@ -85,7 +84,7 @@ describe('sympto', () => { end: { date: '2018-08-05' } }) }) - it('with no shifts detects pre- and peri-ovulatory phase according to 5-day-rule', () => { + it('with no shifts in long cycle detects pre- and peri-ovulatory phase according to 5-day-rule', () => { const status = getSensiplanStatus({ cycle: longCycleWithoutAnyShifts, previousCycle: cervixShiftAndFhmOnSameDay, @@ -105,7 +104,7 @@ describe('sympto', () => { start: { date: '2018-07-06' } }) }) - it('with evaluation of temperature and cervix end on same day', () => { + it('with temperature and cervix evaluation end on same day detects all 3 phases', () => { const status = getSensiplanStatus({ cycle: cervixShiftAndFhmOnSameDay, previousCycle: cervixShiftAndFhmOnSameDay, @@ -135,7 +134,7 @@ describe('sympto', () => { .filter(({date}) => date >= '2018-08-15') }) }) - it('when temperature shift happens 3 days after cervix shift', () => { + it('with temperature shift 3 days after cervix shift detects all 3 phases', () => { const status = getSensiplanStatus({ cycle: tempShift3DaysAfterCervixShift, previousCycle: cervixShiftAndFhmOnSameDay, @@ -167,7 +166,7 @@ describe('sympto', () => { .filter(({date}) => date >= '2018-05-21') }) }) - it('when cervix shift happens 2 days after temperature shift', () => { + it('with cervix shift 2 days after temperature shift detects all 3 phases', () => { const status = getSensiplanStatus({ cycle: cervixShift2DaysAfterTempShift, previousCycle: cervixShiftAndFhmOnSameDay, @@ -198,7 +197,7 @@ describe('sympto', () => { start: { date: '2018-04-19', time: '18:00' } }) }) - it('when no infertile phase can be detected', () => { + it('with no shifts no ovulation is found detects only pre and peri-ovulatory phase', () => { const status = getSensiplanStatus({ cycle: noOvulationDetected, previousCycle: cervixShiftAndFhmOnSameDay, diff --git a/test/sympto/cervix.spec.js b/test/sympto/cervix.spec.js index f935945..f8bbc3e 100644 --- a/test/sympto/cervix.spec.js +++ b/test/sympto/cervix.spec.js @@ -5,87 +5,160 @@ const expect = chai.expect function turnIntoCycleDayObject(value, fakeDate) { const hardAndClosed = { - value: { opening: 0, firmness: 0 } + opening: 0, + firmness: 0 } const hardAndOpen = { - value: { opening: 1, firmness: 0 } + opening: 1, + firmness: 0 } const softAndClosed = { - value: { opening: 0, firmness: 1 } + opening: 0, + firmness: 1 } const softAndOpen = { - value: { opening: 1, firmness: 1 } + opening: 1, + firmness: 1 } const cervixStates = [hardAndClosed, hardAndOpen, softAndClosed, softAndOpen] return { date: fakeDate, - cervix: cervixStates[value], - exclude: false + cervix: { + opening: cervixStates[value].opening, + firmness: cervixStates[value].firmness, + exclude: false + } } } describe('sympto', () => { describe('detects cervix shift', () => { - it('when shift happens at day 15 with consistent following days', function () { - const values = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 3, 1, 3, 1, 0, 0, 0, 0] + it('when shift happens at day 13 with consistent following days of infertile cervix until tempEvalEnd', () => { + const values = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 3, 0, 0, 0, 0, 0, 0, 0] .map(turnIntoCycleDayObject) - const status = getCervixStatus(values) + const status = getCervixStatus(values, 16) expect(status).to.eql({ detected: true, cervixPeakBeforeShift: { - date: 13, - cervix: {value: { opening: 1, firmness: 0 }}, - exclude: false + date: 10, + cervix: { + opening: 1, + firmness: 1, + exclude: false + } }, evaluationCompleteDay: { - date: 16, - cervix: { value: { opening: 0, firmness: 0 }}, - exclude: false + date: 13, + cervix: { + opening: 0, + firmness: 0, + exclude: false + } } }) }) - it('at the very first day of cycle days even if later shift happens again', function () { + it('right at the start of cycle days even if later shift happens again because tempEvalEnd happened before second potential shift', () => { const values = [2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] .map(turnIntoCycleDayObject) - const status = getCervixStatus(values) + const status = getCervixStatus(values, 5) expect(status).to.eql({ detected: true, cervixPeakBeforeShift: { date: 0, - cervix: { value: { opening: 0, firmness: 1 } }, - exclude: false + cervix: { + opening: 0, + firmness: 1, + exclude: false + }, }, evaluationCompleteDay: { date: 3, - cervix: { value: { opening: 0, firmness: 0 } }, - exclude: false + cervix: { + opening: 0, + firmness: 0, + exclude: false + } + } + }) + }) + it('at day 6 although right at the start of cycle days a potential shift happened but because tempEvalEnd happens after second shift', () => { + const values = [2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + .map(turnIntoCycleDayObject) + const status = getCervixStatus(values, 10) + expect(status).to.eql({ + detected: true, + cervixPeakBeforeShift: { + date: 6, + cervix: { + opening: 1, + firmness: 0, + exclude: false + }, + }, + evaluationCompleteDay: { + date: 9, + cervix: { + opening: 0, + firmness: 0, + exclude: false + } + } + }) + }) + it('when the cervix shift is happening after tempEvalEnd', () => { + const values = [1,1,1,1,1,2,3,3,3,3,1,1,1,1,0,0,0,0,0,0,0] + .map(turnIntoCycleDayObject) + const status = getCervixStatus(values, 10) + expect(status).to.eql({ + detected: true, + cervixPeakBeforeShift: { + date: 13, + cervix: { + opening: 1, + firmness: 0, + exclude: false + } + }, + evaluationCompleteDay: { + date: 16, + cervix: { + opening: 0, + firmness: 0, + exclude: false + } } }) }) }) describe('detects no cervix shift', () => { - it('if there are less than 3 days closed and hard cervix', function () { - const values = [0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 3, 1, 1, 1, 0, 0, 2, 0] + it('if there are less than 3 days closed and hard cervix', () => { + const values = [0, 0, 0, 1, 1, 1, 2, 0, 3, 3, 3, 1, 1, 1, 0, 0, 2, 0] .map(turnIntoCycleDayObject) - const status = getCervixStatus(values) + const status = getCervixStatus(values, 15) expect(status).to.eql({ detected: false }) }) - it('if there are no cervix values', function () { - const values = [].map(turnIntoCycleDayObject) - const status = getCervixStatus(values) - expect(status).to.eql({ detected: false }) - }) - it('when the cervix values are all the same', function () { - const values = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + it('if cycleDays have not enough cervix values to detect valid cervix shift', () => { + const values = [2,0,0] .map(turnIntoCycleDayObject) - const status = getCervixStatus(values) + const status = getCervixStatus(values, 17) expect(status).to.eql({ detected: false }) }) - it('if no days of hard and closed cervix are tracked', function () { + it('if no days indicate fertile cervix which could be cervix peak', () => { const values = [1, 3, 2, 1, 3, 2, 1, 3, 2, 1, 3, 2, 1, 3, 2, 1] .map(turnIntoCycleDayObject) - const status = getCervixStatus(values) + const status = getCervixStatus(values, 12) + expect(status).to.eql({ detected: false }) + }) + it('if all days indicate infertile cervix values', () => { + const values = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + .map(turnIntoCycleDayObject) + const status = getCervixStatus(values, 9) + expect(status).to.eql({ detected: false }) + }) + it('if there are no cervix values', () => { + const values = [].map(turnIntoCycleDayObject) + const status = getCervixStatus(values, 15) expect(status).to.eql({ detected: false }) }) })