From 33393f498accb3827650f36501d7b48f91dfd959 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 28 Jun 2018 15:16:21 +0200 Subject: [PATCH 01/59] Extract getLastMensesStart --- lib/get-cycle-day-number.js | 37 ++++++------------------------------ lib/get-last-menses-start.js | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 lib/get-last-menses-start.js diff --git a/lib/get-cycle-day-number.js b/lib/get-cycle-day-number.js index d1b056a..164eb9c 100644 --- a/lib/get-cycle-day-number.js +++ b/lib/get-cycle-day-number.js @@ -1,6 +1,5 @@ import * as joda from 'js-joda' - -const LocalDate = joda.LocalDate +import getLastMensesStart from './get-last-menses-start' export default function config(opts = {}) { let bleedingDaysSortedByDate @@ -12,37 +11,13 @@ export default function config(opts = {}) { } const maxBreakInBleeding = opts.maxBreakInBleeding || 1 - return function getCycleDayNumber(targetDateString) { - const targetDate = LocalDate.parse(targetDateString) - const withWrappedDates = bleedingDaysSortedByDate - .filter(day => !day.bleeding.exclude) - .map(day => { - day.wrappedDate = LocalDate.parse(day.date) - return day - }) - - const firstBleedingDayBeforeTargetDayIndex = withWrappedDates.findIndex(day => { - return ( - day.wrappedDate.isEqual(targetDate) || - day.wrappedDate.isBefore(targetDate) - ) - }) - - if (firstBleedingDayBeforeTargetDayIndex < 0) return null - const previousBleedingDays = withWrappedDates.slice(firstBleedingDayBeforeTargetDayIndex) - - const lastPeriodStart = previousBleedingDays.find((day, i) => { - return thereIsNoPreviousBleedingDayWithinTheThreshold(day, previousBleedingDays.slice(i + 1), maxBreakInBleeding) - }) - - const diffInDays = lastPeriodStart.wrappedDate.until(targetDate, joda.ChronoUnit.DAYS) + return function(targetDateString) { + const lastMensesStart = getLastMensesStart(targetDateString, bleedingDaysSortedByDate, maxBreakInBleeding) + if (!lastMensesStart) return null + const targetDate = joda.LocalDate.parse(targetDateString) + const diffInDays = lastMensesStart.wrappedDate.until(targetDate, joda.ChronoUnit.DAYS) // cycle starts at day 1 return diffInDays + 1 } -} - -function thereIsNoPreviousBleedingDayWithinTheThreshold(bleedingDay, earlierCycleDays, allowedBleedingBreak) { - const periodThreshold = bleedingDay.wrappedDate.minusDays(allowedBleedingBreak + 1) - return !earlierCycleDays.some(({ wrappedDate }) => wrappedDate.equals(periodThreshold) || wrappedDate.isAfter(periodThreshold)) } \ No newline at end of file diff --git a/lib/get-last-menses-start.js b/lib/get-last-menses-start.js new file mode 100644 index 0000000..7fb321f --- /dev/null +++ b/lib/get-last-menses-start.js @@ -0,0 +1,34 @@ +import * as joda from 'js-joda' + +const LocalDate = joda.LocalDate + +export default function getLastMensesStart(targetDateString, bleedingDaysSortedByDate, maxBreakInBleeding) { + const targetDate = LocalDate.parse(targetDateString) + const withWrappedDates = bleedingDaysSortedByDate + .filter(day => !day.bleeding.exclude) + .map(day => { + day.wrappedDate = LocalDate.parse(day.date) + return day + }) + + const firstBleedingDayBeforeTargetDayIndex = withWrappedDates.findIndex(day => { + return ( + day.wrappedDate.isEqual(targetDate) || + day.wrappedDate.isBefore(targetDate) + ) + }) + + if (firstBleedingDayBeforeTargetDayIndex < 0) return null + const previousBleedingDays = withWrappedDates.slice(firstBleedingDayBeforeTargetDayIndex) + + const lastPeriodStart = previousBleedingDays.find((day, i) => { + return thereIsNoPreviousBleedingDayWithinTheThreshold(day, previousBleedingDays.slice(i + 1), maxBreakInBleeding) + }) + + return lastPeriodStart +} + +function thereIsNoPreviousBleedingDayWithinTheThreshold(bleedingDay, earlierCycleDays, allowedBleedingBreak) { + const periodThreshold = bleedingDay.wrappedDate.minusDays(allowedBleedingBreak + 1) + return !earlierCycleDays.some(({ wrappedDate }) => wrappedDate.equals(periodThreshold) || wrappedDate.isAfter(periodThreshold)) +} \ No newline at end of file From 97b8452b45a9ad524d848f510089c5c55ed71a30 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 28 Jun 2018 15:59:42 +0200 Subject: [PATCH 02/59] Add first failing test --- lib/sensiplan/index.js | 7 +++++++ test/fixtures/regular-cycles.json | 1 + test/sensiplan.spec.js | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 lib/sensiplan/index.js create mode 100644 test/fixtures/regular-cycles.json create mode 100644 test/sensiplan.spec.js diff --git a/lib/sensiplan/index.js b/lib/sensiplan/index.js new file mode 100644 index 0000000..0d733f4 --- /dev/null +++ b/lib/sensiplan/index.js @@ -0,0 +1,7 @@ +function getTemperatureStatus (targetDate) { + return '42' +} + +export { + getTemperatureStatus +} \ No newline at end of file diff --git a/test/fixtures/regular-cycles.json b/test/fixtures/regular-cycles.json new file mode 100644 index 0000000..38b534c --- /dev/null +++ b/test/fixtures/regular-cycles.json @@ -0,0 +1 @@ +[{"date":"2018-04-30","bleeding":{"value":2}},{"date":"2018-05-08","temperature":{"value":36.48,"exclude":false}},{"date":"2018-05-09","temperature":{"value":36.46,"exclude":false}},{"date":"2018-05-10","temperature":{"value":36.4,"exclude":false}},{"date":"2018-05-11","temperature":{"value":36.38,"exclude":false}},{"date":"2018-05-12","temperature":{"value":36.57,"exclude":false},"bleeding":{"value":4}},{"date":"2018-05-13","temperature":{"value":36.51,"exclude":false},"bleeding":{"value":4}},{"date":"2018-05-14","temperature":{"value":36.64,"exclude":false},"bleeding":{"value":4}},{"date":"2018-05-15","temperature":{"value":36.5,"exclude":false},"bleeding":{"value":3}},{"date":"2018-05-16","temperature":{"value":36.74,"exclude":false}},{"date":"2018-05-17","temperature":{"value":36.74,"exclude":false}},{"date":"2018-05-18","temperature":{"value":36.85,"exclude":false}},{"date":"2018-05-29","bleeding":{"value":2}},{"date":"2018-06-04","temperature":{"value":36.7,"exclude":false}},{"date":"2018-06-05","temperature":{"value":36.57,"exclude":false}},{"date":"2018-06-06","temperature":{"value":36.47,"exclude":false}},{"date":"2018-06-07","temperature":{"value":36.49,"exclude":false}},{"date":"2018-06-08","bleeding":{"value":4}},{"date":"2018-06-09","temperature":{"value":36.57,"exclude":false},"bleeding":{"value":4}},{"date":"2018-06-10","temperature":{"value":36.62,"exclude":false},"bleeding":{"value":4}},{"date":"2018-06-11","temperature":{"value":36.55,"exclude":false},"bleeding":{"value":3}},{"date":"2018-06-12","temperature":{"value":36.8,"exclude":false}},{"date":"2018-06-13","temperature":{"value":36.86,"exclude":false}},{"date":"2018-06-14","temperature":{"value":36.82,"exclude":false}},{"date":"2018-06-24","bleeding":{"value":2}}] \ No newline at end of file diff --git a/test/sensiplan.spec.js b/test/sensiplan.spec.js new file mode 100644 index 0000000..cd36646 --- /dev/null +++ b/test/sensiplan.spec.js @@ -0,0 +1,20 @@ +import chai from 'chai' +import * as sensiplan from '../lib/sensiplan' +import cycleDaysFixtures from './fixtures/regular-cycles.json' + +const expect = chai.expect + +describe('sensiplan', () => { + describe('getTemperatureStatus', () => { + it('detects temperature shift', function () { + const targetDate = '2018-06-14' + const status = sensiplan.getTemperatureStatus(targetDate, cycleDaysFixtures) + expect(status).to.eql({ + lowerTemps: [36.6, 36.5, 36.5, 36.6, 36.6, 36.6], + ltl: 36.6, + higherTemps: [36.8, 36.9, 36.8], + shiftDetected: true + }) + }) + }) +}) \ No newline at end of file From 28b880a40cef7879985af92d85824ffd58035e59 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Sat, 30 Jun 2018 13:50:01 +0200 Subject: [PATCH 03/59] Gather general cycle-related functions in cycle module --- components/chart/chart.js | 4 +- components/cycle-day-overview.js | 6 +- components/cycle-day.js | 4 +- components/home.js | 4 +- lib/cycle.js | 65 +++++++++++++++++++ lib/get-cycle-day-number.js | 23 ------- lib/get-last-menses-start.js | 34 ---------- test/{get-cycle-day.spec.js => cycle.spec.js} | 19 +++--- 8 files changed, 84 insertions(+), 75 deletions(-) create mode 100644 lib/cycle.js delete mode 100644 lib/get-cycle-day-number.js delete mode 100644 lib/get-last-menses-start.js rename test/{get-cycle-day.spec.js => cycle.spec.js} (77%) diff --git a/components/chart/chart.js b/components/chart/chart.js index 2c70ed1..877d231 100644 --- a/components/chart/chart.js +++ b/components/chart/chart.js @@ -11,11 +11,11 @@ import Svg,{ } from 'react-native-svg' import { LocalDate } from 'js-joda' import { getCycleDay, getOrCreateCycleDay, cycleDaysSortedByDate } from '../../db' -import getCycleDayNumberModule from '../../lib/get-cycle-day-number' +import cycleModule from '../../lib/cycle' import styles from './styles' import config from './config' -const getCycleDayNumber = getCycleDayNumberModule() +const getCycleDayNumber = cycleModule().getCycleDayNumber const yAxis = makeYAxis(config) diff --git a/components/cycle-day-overview.js b/components/cycle-day-overview.js index cc780da..f8c3aca 100644 --- a/components/cycle-day-overview.js +++ b/components/cycle-day-overview.js @@ -6,14 +6,16 @@ import { } from 'react-native' import styles from '../styles/index' import { bleeding as labels} from '../labels/labels' -import cycleDayModule from '../lib/get-cycle-day-number' +import cycleModule from '../lib/cycle' import { bleedingDaysSortedByDate } from '../db' -const getCycleDayNumber = cycleDayModule() +const getCycleDayNumber = cycleModule().getCycleDayNumber export default class DayView extends Component { constructor(props) { super(props) + console.log('new') + console.log(props.cycleDay) this.cycleDay = props.cycleDay this.showView = props.showView this.state = { diff --git a/components/cycle-day.js b/components/cycle-day.js index 7d46dea..2d78220 100644 --- a/components/cycle-day.js +++ b/components/cycle-day.js @@ -3,14 +3,14 @@ import { View, Text } from 'react-native' -import cycleDayModule from '../lib/get-cycle-day-number' +import cycleModule from '../lib/cycle' import DayView from './cycle-day-overview' import BleedingEditView from './bleeding' import TemperatureEditView from './temperature' import { formatDateForViewHeader } from '../labels/format' import styles from '../styles/index' -const getCycleDayNumber = cycleDayModule() +const getCycleDayNumber = cycleModule().getCycleDayNumber export default class Day extends Component { constructor(props) { diff --git a/components/home.js b/components/home.js index 60327e3..816a10c 100644 --- a/components/home.js +++ b/components/home.js @@ -6,10 +6,10 @@ import { } from 'react-native' import { LocalDate } from 'js-joda' import styles from '../styles/index' -import cycleDayModule from '../lib/get-cycle-day-number' +import cycleModule from '../lib/cycle' import { getOrCreateCycleDay, bleedingDaysSortedByDate, deleteAll } from '../db' -const getCycleDayNumber = cycleDayModule() +const getCycleDayNumber = cycleModule().getCycleDayNumber export default class Home extends Component { constructor(props) { diff --git a/lib/cycle.js b/lib/cycle.js new file mode 100644 index 0000000..8dbe838 --- /dev/null +++ b/lib/cycle.js @@ -0,0 +1,65 @@ +import * as joda from 'js-joda' + +const LocalDate = joda.LocalDate + +export default function config(opts = {}) { + let bleedingDaysSortedByDate + if (!opts.bleedingDaysSortedByDate) { + // we only want to require (and run) the db module when not running the tests + bleedingDaysSortedByDate = require('../db').bleedingDaysSortedByDate + } else { + bleedingDaysSortedByDate = opts.bleedingDaysSortedByDate + } + const maxBreakInBleeding = opts.maxBreakInBleeding || 1 + + function getLastMensesStart(targetDateString) { + const targetDate = LocalDate.parse(targetDateString) + const withWrappedDates = bleedingDaysSortedByDate + .filter(day => !day.bleeding.exclude) + .map(day => { + day.wrappedDate = LocalDate.parse(day.date) + return day + }) + + const firstBleedingDayBeforeTargetDayIndex = withWrappedDates.findIndex(day => { + return ( + day.wrappedDate.isEqual(targetDate) || + day.wrappedDate.isBefore(targetDate) + ) + }) + + if (firstBleedingDayBeforeTargetDayIndex < 0) return null + const previousBleedingDays = withWrappedDates.slice(firstBleedingDayBeforeTargetDayIndex) + + const lastPeriodStart = previousBleedingDays.find((day, i) => { + return thereIsNoPreviousBleedingDayWithinTheThreshold(day, previousBleedingDays.slice(i + 1)) + }) + + function thereIsNoPreviousBleedingDayWithinTheThreshold(bleedingDay, previousBleedingDays) { + const periodThreshold = bleedingDay.wrappedDate.minusDays(maxBreakInBleeding + 1) + return !previousBleedingDays.some(({ wrappedDate }) => wrappedDate.equals(periodThreshold) || wrappedDate.isAfter(periodThreshold)) + } + + return lastPeriodStart + } + + function getCycleDayNumber(targetDateString) { + const lastMensesStart = getLastMensesStart(targetDateString) + if (!lastMensesStart) return null + const targetDate = joda.LocalDate.parse(targetDateString) + const diffInDays = lastMensesStart.wrappedDate.until(targetDate, joda.ChronoUnit.DAYS) + + // cycle starts at day 1 + return diffInDays + 1 + } + + function getPreviousDaysInCycle() { + return [] + } + + return { + getCycleDayNumber, + getLastMensesStart, + getPreviousDaysInCycle + } +} diff --git a/lib/get-cycle-day-number.js b/lib/get-cycle-day-number.js deleted file mode 100644 index 164eb9c..0000000 --- a/lib/get-cycle-day-number.js +++ /dev/null @@ -1,23 +0,0 @@ -import * as joda from 'js-joda' -import getLastMensesStart from './get-last-menses-start' - -export default function config(opts = {}) { - let bleedingDaysSortedByDate - if (!opts.bleedingDaysSortedByDate) { - // we only want to require (and run) the db module when not running the tests - bleedingDaysSortedByDate = require('../db').bleedingDaysSortedByDate - } else { - bleedingDaysSortedByDate = opts.bleedingDaysSortedByDate - } - const maxBreakInBleeding = opts.maxBreakInBleeding || 1 - - return function(targetDateString) { - const lastMensesStart = getLastMensesStart(targetDateString, bleedingDaysSortedByDate, maxBreakInBleeding) - if (!lastMensesStart) return null - const targetDate = joda.LocalDate.parse(targetDateString) - const diffInDays = lastMensesStart.wrappedDate.until(targetDate, joda.ChronoUnit.DAYS) - - // cycle starts at day 1 - return diffInDays + 1 - } -} \ No newline at end of file diff --git a/lib/get-last-menses-start.js b/lib/get-last-menses-start.js deleted file mode 100644 index 7fb321f..0000000 --- a/lib/get-last-menses-start.js +++ /dev/null @@ -1,34 +0,0 @@ -import * as joda from 'js-joda' - -const LocalDate = joda.LocalDate - -export default function getLastMensesStart(targetDateString, bleedingDaysSortedByDate, maxBreakInBleeding) { - const targetDate = LocalDate.parse(targetDateString) - const withWrappedDates = bleedingDaysSortedByDate - .filter(day => !day.bleeding.exclude) - .map(day => { - day.wrappedDate = LocalDate.parse(day.date) - return day - }) - - const firstBleedingDayBeforeTargetDayIndex = withWrappedDates.findIndex(day => { - return ( - day.wrappedDate.isEqual(targetDate) || - day.wrappedDate.isBefore(targetDate) - ) - }) - - if (firstBleedingDayBeforeTargetDayIndex < 0) return null - const previousBleedingDays = withWrappedDates.slice(firstBleedingDayBeforeTargetDayIndex) - - const lastPeriodStart = previousBleedingDays.find((day, i) => { - return thereIsNoPreviousBleedingDayWithinTheThreshold(day, previousBleedingDays.slice(i + 1), maxBreakInBleeding) - }) - - return lastPeriodStart -} - -function thereIsNoPreviousBleedingDayWithinTheThreshold(bleedingDay, earlierCycleDays, allowedBleedingBreak) { - const periodThreshold = bleedingDay.wrappedDate.minusDays(allowedBleedingBreak + 1) - return !earlierCycleDays.some(({ wrappedDate }) => wrappedDate.equals(periodThreshold) || wrappedDate.isAfter(periodThreshold)) -} \ No newline at end of file diff --git a/test/get-cycle-day.spec.js b/test/cycle.spec.js similarity index 77% rename from test/get-cycle-day.spec.js rename to test/cycle.spec.js index 8aaaddd..ae60632 100644 --- a/test/get-cycle-day.spec.js +++ b/test/cycle.spec.js @@ -1,13 +1,12 @@ import chai from 'chai' import dirtyChai from 'dirty-chai' +import cycleModule from '../lib/cycle' const expect = chai.expect chai.use(dirtyChai) -import getCycleDayNumberModule from '../lib/get-cycle-day-number' - describe('getCycleDay', () => { - it('works for a simple example', function () { + it('works for a simple example', () => { const bleedingDays = [{ date: '2018-05-10', bleeding: { @@ -24,7 +23,7 @@ describe('getCycleDay', () => { value: 2 } }] - const getCycleDayNumber = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays}) + const getCycleDayNumber = cycleModule({bleedingDaysSortedByDate: bleedingDays}).getCycleDayNumber const targetDate = '2018-05-17' const result = getCycleDayNumber(targetDate) expect(result).to.eql(9) @@ -50,7 +49,7 @@ describe('getCycleDay', () => { } }] const targetDate = '2018-05-17' - const getCycleDayNumber = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays}) + const getCycleDayNumber = cycleModule({bleedingDaysSortedByDate: bleedingDays}).getCycleDayNumber const result = getCycleDayNumber(targetDate) expect(result).to.eql(15) }) @@ -74,7 +73,7 @@ describe('getCycleDay', () => { }] const targetDate = '2018-04-27' - const getCycleDayNumber = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays}) + const getCycleDayNumber = cycleModule({bleedingDaysSortedByDate: bleedingDays}).getCycleDayNumber const result = getCycleDayNumber(targetDate) expect(result).to.eql(18) }) @@ -88,7 +87,7 @@ describe('getCycleDay', () => { }] const targetDate = '2018-05-13' - const getCycleDayNumber = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays}) + const getCycleDayNumber = cycleModule({bleedingDaysSortedByDate: bleedingDays}).getCycleDayNumber const result = getCycleDayNumber(targetDate) expect(result).to.eql(1) }) @@ -98,7 +97,7 @@ describe('getCycleDay returns null', () => { it('if there are no bleeding days', function () { const bleedingDays = [] const targetDate = '2018-05-17' - const getCycleDayNumber = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays}) + const getCycleDayNumber = cycleModule({bleedingDaysSortedByDate: bleedingDays}).getCycleDayNumber const result = getCycleDayNumber(targetDate) expect(result).to.be.null() }) @@ -121,7 +120,7 @@ describe('getCycleDay with cycle thresholds', () => { }] const targetDate = '2018-05-17' - const getCycleDayNumber = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays, maxBreakInBleeding }) + const getCycleDayNumber = cycleModule({bleedingDaysSortedByDate: bleedingDays, maxBreakInBleeding }).getCycleDayNumber const result = getCycleDayNumber(targetDate) expect(result).to.eql(8) }) @@ -139,7 +138,7 @@ describe('getCycleDay with cycle thresholds', () => { } }] const targetDate = '2018-05-17' - const getCycleDayNumber = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays, maxBreakInBleeding }) + const getCycleDayNumber = cycleModule({bleedingDaysSortedByDate: bleedingDays, maxBreakInBleeding }).getCycleDayNumber const result = getCycleDayNumber(targetDate) expect(result).to.eql(4) }) From 3a090c5d8a89e8552fcf461b8663e1c5fa7f530f Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Sat, 30 Jun 2018 14:57:51 +0200 Subject: [PATCH 04/59] For temperature, add 2 passing test for simple happy path --- lib/sensiplan.js | 39 ++++++++++++++++++++ lib/sensiplan/index.js | 7 ---- test/fixtures/lower-temps.json | 31 ++++++++++++++++ test/fixtures/regular-cycles.json | 1 - test/fixtures/temp-shift.json | 61 +++++++++++++++++++++++++++++++ test/sensiplan.spec.js | 26 +++++++++---- 6 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 lib/sensiplan.js delete mode 100644 lib/sensiplan/index.js create mode 100644 test/fixtures/lower-temps.json delete mode 100644 test/fixtures/regular-cycles.json create mode 100644 test/fixtures/temp-shift.json diff --git a/lib/sensiplan.js b/lib/sensiplan.js new file mode 100644 index 0000000..1539f3a --- /dev/null +++ b/lib/sensiplan.js @@ -0,0 +1,39 @@ +function getTemperatureStatus(targetDateString, previousDaysInCycle) { + const tempValues = previousDaysInCycle + .filter(day => day.temperature) + .map(day => !day.temperature.exclude && rounded(day.temperature.value, 0.05)) + + return tempValues.reduce((acc, curr) => { + // if we don't yet have 6 lower temps, we just collect + // if no shift has been detected, we collect low temps + // after the shift has been detected, we count them as part + // of the higher temperature phase + if (acc.low.length < 6 || (!acc.shiftDetected && curr <= acc.ltl)) { + acc.low.push(curr) + acc.ltl = Math.max(...acc.low.slice(-6)) + } else { + acc.high.push(curr) + } + + // we round the difference because of JS decimal weirdness + if (acc.high.length === 3 && rounded(curr - acc.ltl, 0.01) >= 0.2) { + acc.shiftDetected = true + } + + return acc + }, { + low: [], + high: [], + ltl: null, + shiftDetected: false + }) +} + +function rounded(val, step) { + const inverted = 1 / step + return Math.round(val * inverted) / inverted +} + +export { + getTemperatureStatus +} \ No newline at end of file diff --git a/lib/sensiplan/index.js b/lib/sensiplan/index.js deleted file mode 100644 index 0d733f4..0000000 --- a/lib/sensiplan/index.js +++ /dev/null @@ -1,7 +0,0 @@ -function getTemperatureStatus (targetDate) { - return '42' -} - -export { - getTemperatureStatus -} \ No newline at end of file diff --git a/test/fixtures/lower-temps.json b/test/fixtures/lower-temps.json new file mode 100644 index 0000000..20ae088 --- /dev/null +++ b/test/fixtures/lower-temps.json @@ -0,0 +1,31 @@ +[{ + "date": "2018-06-04", + "temperature": { + "value": 36.7, + "exclude": null + } +}, { + "date": "2018-06-05", + "temperature": { + "value": 36.57, + "exclude": null + } +}, { + "date": "2018-06-06", + "temperature": { + "value": 36.47, + "exclude": null + } +}, { + "date": "2018-06-07", + "temperature": { + "value": 36.49, + "exclude": null + } +}, { + "date": "2018-06-09", + "temperature": { + "value": 36.57, + "exclude": null + } +}] \ No newline at end of file diff --git a/test/fixtures/regular-cycles.json b/test/fixtures/regular-cycles.json deleted file mode 100644 index 38b534c..0000000 --- a/test/fixtures/regular-cycles.json +++ /dev/null @@ -1 +0,0 @@ -[{"date":"2018-04-30","bleeding":{"value":2}},{"date":"2018-05-08","temperature":{"value":36.48,"exclude":false}},{"date":"2018-05-09","temperature":{"value":36.46,"exclude":false}},{"date":"2018-05-10","temperature":{"value":36.4,"exclude":false}},{"date":"2018-05-11","temperature":{"value":36.38,"exclude":false}},{"date":"2018-05-12","temperature":{"value":36.57,"exclude":false},"bleeding":{"value":4}},{"date":"2018-05-13","temperature":{"value":36.51,"exclude":false},"bleeding":{"value":4}},{"date":"2018-05-14","temperature":{"value":36.64,"exclude":false},"bleeding":{"value":4}},{"date":"2018-05-15","temperature":{"value":36.5,"exclude":false},"bleeding":{"value":3}},{"date":"2018-05-16","temperature":{"value":36.74,"exclude":false}},{"date":"2018-05-17","temperature":{"value":36.74,"exclude":false}},{"date":"2018-05-18","temperature":{"value":36.85,"exclude":false}},{"date":"2018-05-29","bleeding":{"value":2}},{"date":"2018-06-04","temperature":{"value":36.7,"exclude":false}},{"date":"2018-06-05","temperature":{"value":36.57,"exclude":false}},{"date":"2018-06-06","temperature":{"value":36.47,"exclude":false}},{"date":"2018-06-07","temperature":{"value":36.49,"exclude":false}},{"date":"2018-06-08","bleeding":{"value":4}},{"date":"2018-06-09","temperature":{"value":36.57,"exclude":false},"bleeding":{"value":4}},{"date":"2018-06-10","temperature":{"value":36.62,"exclude":false},"bleeding":{"value":4}},{"date":"2018-06-11","temperature":{"value":36.55,"exclude":false},"bleeding":{"value":3}},{"date":"2018-06-12","temperature":{"value":36.8,"exclude":false}},{"date":"2018-06-13","temperature":{"value":36.86,"exclude":false}},{"date":"2018-06-14","temperature":{"value":36.82,"exclude":false}},{"date":"2018-06-24","bleeding":{"value":2}}] \ No newline at end of file diff --git a/test/fixtures/temp-shift.json b/test/fixtures/temp-shift.json new file mode 100644 index 0000000..74bd559 --- /dev/null +++ b/test/fixtures/temp-shift.json @@ -0,0 +1,61 @@ +[{ + "date": "2018-06-04", + "temperature": { + "value": 36.7, + "exclude": null + } +}, { + "date": "2018-06-05", + "temperature": { + "value": 36.57, + "exclude": null + } +}, { + "date": "2018-06-06", + "temperature": { + "value": 36.47, + "exclude": null + } +}, { + "date": "2018-06-07", + "temperature": { + "value": 36.49, + "exclude": null + } +}, { + "date": "2018-06-09", + "temperature": { + "value": 36.57, + "exclude": null + } +}, { + "date": "2018-06-10", + "temperature": { + "value": 36.62, + "exclude": null + } +}, { + "date": "2018-06-11", + "temperature": { + "value": 36.55, + "exclude": null + } +}, { + "date": "2018-06-12", + "temperature": { + "value": 36.8, + "exclude": null + } +}, { + "date": "2018-06-13", + "temperature": { + "value": 36.86, + "exclude": null + } +}, { + "date": "2018-06-14", + "temperature": { + "value": 36.8, + "exclude": null + } +}] \ No newline at end of file diff --git a/test/sensiplan.spec.js b/test/sensiplan.spec.js index cd36646..3ca639e 100644 --- a/test/sensiplan.spec.js +++ b/test/sensiplan.spec.js @@ -1,18 +1,28 @@ import chai from 'chai' -import * as sensiplan from '../lib/sensiplan' -import cycleDaysFixtures from './fixtures/regular-cycles.json' +import { getTemperatureStatus } from '../lib/sensiplan' +import tempShift from './fixtures/temp-shift.json' +import lowerTempDays from './fixtures/lower-temps.json' const expect = chai.expect -describe('sensiplan', () => { +describe.only('sensiplan', () => { describe('getTemperatureStatus', () => { - it('detects temperature shift', function () { - const targetDate = '2018-06-14' - const status = sensiplan.getTemperatureStatus(targetDate, cycleDaysFixtures) + it('reports lower temperature status before shift', function () { + const status = getTemperatureStatus('2018-06-09', lowerTempDays) expect(status).to.eql({ - lowerTemps: [36.6, 36.5, 36.5, 36.6, 36.6, 36.6], + low: [36.7, 36.55, 36.45, 36.5, 36.55], + ltl: 36.7, + high: [], + shiftDetected: false + }) + }) + + it('detects temperature shift', function () { + const status = getTemperatureStatus('2018-06-14', tempShift) + expect(status).to.eql({ + low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, - higherTemps: [36.8, 36.9, 36.8], + high: [36.8, 36.85, 36.8], shiftDetected: true }) }) From 9e92f072deaec5cdffe349ef08e6bc19839df562 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Sat, 30 Jun 2018 15:16:29 +0200 Subject: [PATCH 05/59] Add 1st exception rule --- lib/sensiplan.js | 5 ++ test/fixtures/first-exception-rule.json | 67 +++++++++++++++++++ .../{temp-shift.json => regular-rule.json} | 0 test/sensiplan.spec.js | 15 ++++- 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/first-exception-rule.json rename test/fixtures/{temp-shift.json => regular-rule.json} (100%) diff --git a/lib/sensiplan.js b/lib/sensiplan.js index 1539f3a..a9ad09d 100644 --- a/lib/sensiplan.js +++ b/lib/sensiplan.js @@ -15,10 +15,15 @@ function getTemperatureStatus(targetDateString, previousDaysInCycle) { acc.high.push(curr) } + // regular rule // we round the difference because of JS decimal weirdness if (acc.high.length === 3 && rounded(curr - acc.ltl, 0.01) >= 0.2) { acc.shiftDetected = true } + // 1st exception rule + if (acc.high.length === 4 && curr > acc.ltl) { + acc.shiftDetected = true + } return acc }, { diff --git a/test/fixtures/first-exception-rule.json b/test/fixtures/first-exception-rule.json new file mode 100644 index 0000000..2b963ef --- /dev/null +++ b/test/fixtures/first-exception-rule.json @@ -0,0 +1,67 @@ +[{ + "date": "2018-06-04", + "temperature": { + "value": 36.7, + "exclude": null + } +}, { + "date": "2018-06-05", + "temperature": { + "value": 36.57, + "exclude": null + } +}, { + "date": "2018-06-06", + "temperature": { + "value": 36.47, + "exclude": null + } +}, { + "date": "2018-06-07", + "temperature": { + "value": 36.49, + "exclude": null + } +}, { + "date": "2018-06-09", + "temperature": { + "value": 36.57, + "exclude": null + } +}, { + "date": "2018-06-10", + "temperature": { + "value": 36.62, + "exclude": null + } +}, { + "date": "2018-06-11", + "temperature": { + "value": 36.55, + "exclude": null + } +}, { + "date": "2018-06-12", + "temperature": { + "value": 36.8, + "exclude": null + } +}, { + "date": "2018-06-13", + "temperature": { + "value": 36.86, + "exclude": null + } +}, { + "date": "2018-06-14", + "temperature": { + "value": 36.77, + "exclude": null + } +}, { + "date": "2018-06-15", + "temperature": { + "value": 36.63, + "exclude": null + } +}] \ No newline at end of file diff --git a/test/fixtures/temp-shift.json b/test/fixtures/regular-rule.json similarity index 100% rename from test/fixtures/temp-shift.json rename to test/fixtures/regular-rule.json diff --git a/test/sensiplan.spec.js b/test/sensiplan.spec.js index 3ca639e..25ab34e 100644 --- a/test/sensiplan.spec.js +++ b/test/sensiplan.spec.js @@ -1,7 +1,8 @@ import chai from 'chai' import { getTemperatureStatus } from '../lib/sensiplan' -import tempShift from './fixtures/temp-shift.json' +import tempShift from './fixtures/regular-rule.json' import lowerTempDays from './fixtures/lower-temps.json' +import firstException from './fixtures/first-exception-rule.json' const expect = chai.expect @@ -17,7 +18,7 @@ describe.only('sensiplan', () => { }) }) - it('detects temperature shift', function () { + it('detects temperature shift according to regular rule', function () { const status = getTemperatureStatus('2018-06-14', tempShift) expect(status).to.eql({ low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], @@ -26,5 +27,15 @@ describe.only('sensiplan', () => { shiftDetected: true }) }) + + it('detects temperature shift according to 1st exception rule', function () { + const status = getTemperatureStatus('2018-06-14', firstException) + expect(status).to.eql({ + low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], + ltl: 36.6, + high: [36.8, 36.85, 36.75, 36.65], + shiftDetected: true + }) + }) }) }) \ No newline at end of file From e1f473de3b1b609cf8947bd95c0a3cd9cf0329b2 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Sat, 30 Jun 2018 16:01:12 +0200 Subject: [PATCH 06/59] Detect missing shifts --- lib/sensiplan.js | 11 ++- .../first-exception-rule-no-shift.json | 67 +++++++++++++++++ test/fixtures/regular-rule-no-shift.json | 61 ++++++++++++++++ ...ular-rule.json => regular-rule-shift.json} | 0 test/sensiplan.spec.js | 72 +++++++++++++------ 5 files changed, 187 insertions(+), 24 deletions(-) create mode 100644 test/fixtures/first-exception-rule-no-shift.json create mode 100644 test/fixtures/regular-rule-no-shift.json rename test/fixtures/{regular-rule.json => regular-rule-shift.json} (100%) diff --git a/lib/sensiplan.js b/lib/sensiplan.js index a9ad09d..1364665 100644 --- a/lib/sensiplan.js +++ b/lib/sensiplan.js @@ -3,15 +3,22 @@ function getTemperatureStatus(targetDateString, previousDaysInCycle) { .filter(day => day.temperature) .map(day => !day.temperature.exclude && rounded(day.temperature.value, 0.05)) + let detectingPotentialHighLevel = false + return tempValues.reduce((acc, curr) => { // if we don't yet have 6 lower temps, we just collect // if no shift has been detected, we collect low temps // after the shift has been detected, we count them as part // of the higher temperature phase - if (acc.low.length < 6 || (!acc.shiftDetected && curr <= acc.ltl)) { + if (acc.low.length < 7) { + acc.low.push(curr) + acc.ltl = Math.max(...acc.low.slice(-6)) + // TODO these are the same + } else if (curr <= acc.ltl && !detectingPotentialHighLevel && !acc.shiftDetected) { acc.low.push(curr) acc.ltl = Math.max(...acc.low.slice(-6)) } else { + detectingPotentialHighLevel = true acc.high.push(curr) } @@ -19,10 +26,12 @@ function getTemperatureStatus(targetDateString, previousDaysInCycle) { // we round the difference because of JS decimal weirdness if (acc.high.length === 3 && rounded(curr - acc.ltl, 0.01) >= 0.2) { acc.shiftDetected = true + detectingPotentialHighLevel = false } // 1st exception rule if (acc.high.length === 4 && curr > acc.ltl) { acc.shiftDetected = true + detectingPotentialHighLevel = false } return acc diff --git a/test/fixtures/first-exception-rule-no-shift.json b/test/fixtures/first-exception-rule-no-shift.json new file mode 100644 index 0000000..559aefb --- /dev/null +++ b/test/fixtures/first-exception-rule-no-shift.json @@ -0,0 +1,67 @@ +[{ + "date": "2018-06-04", + "temperature": { + "value": 36.7, + "exclude": null + } +}, { + "date": "2018-06-05", + "temperature": { + "value": 36.57, + "exclude": null + } +}, { + "date": "2018-06-06", + "temperature": { + "value": 36.47, + "exclude": null + } +}, { + "date": "2018-06-07", + "temperature": { + "value": 36.49, + "exclude": null + } +}, { + "date": "2018-06-09", + "temperature": { + "value": 36.57, + "exclude": null + } +}, { + "date": "2018-06-10", + "temperature": { + "value": 36.62, + "exclude": null + } +}, { + "date": "2018-06-11", + "temperature": { + "value": 36.55, + "exclude": null + } +}, { + "date": "2018-06-12", + "temperature": { + "value": 36.8, + "exclude": null + } +}, { + "date": "2018-06-13", + "temperature": { + "value": 36.86, + "exclude": null + } +}, { + "date": "2018-06-14", + "temperature": { + "value": 36.77, + "exclude": null + } +}, { + "date": "2018-06-15", + "temperature": { + "value": 36.57, + "exclude": null + } +}] \ No newline at end of file diff --git a/test/fixtures/regular-rule-no-shift.json b/test/fixtures/regular-rule-no-shift.json new file mode 100644 index 0000000..ca99c5a --- /dev/null +++ b/test/fixtures/regular-rule-no-shift.json @@ -0,0 +1,61 @@ +[{ + "date": "2018-06-04", + "temperature": { + "value": 36.7, + "exclude": null + } +}, { + "date": "2018-06-05", + "temperature": { + "value": 36.57, + "exclude": null + } +}, { + "date": "2018-06-06", + "temperature": { + "value": 36.47, + "exclude": null + } +}, { + "date": "2018-06-07", + "temperature": { + "value": 36.49, + "exclude": null + } +}, { + "date": "2018-06-09", + "temperature": { + "value": 36.57, + "exclude": null + } +}, { + "date": "2018-06-10", + "temperature": { + "value": 36.62, + "exclude": null + } +}, { + "date": "2018-06-11", + "temperature": { + "value": 36.55, + "exclude": null + } +}, { + "date": "2018-06-12", + "temperature": { + "value": 36.8, + "exclude": null + } +}, { + "date": "2018-06-13", + "temperature": { + "value": 36.86, + "exclude": null + } +}, { + "date": "2018-06-14", + "temperature": { + "value": 36.77, + "exclude": null + } +}] \ No newline at end of file diff --git a/test/fixtures/regular-rule.json b/test/fixtures/regular-rule-shift.json similarity index 100% rename from test/fixtures/regular-rule.json rename to test/fixtures/regular-rule-shift.json diff --git a/test/sensiplan.spec.js b/test/sensiplan.spec.js index 25ab34e..055f6db 100644 --- a/test/sensiplan.spec.js +++ b/test/sensiplan.spec.js @@ -1,40 +1,66 @@ import chai from 'chai' import { getTemperatureStatus } from '../lib/sensiplan' -import tempShift from './fixtures/regular-rule.json' +import tempShift from './fixtures/regular-rule-shift.json' +import noTempShift from './fixtures/regular-rule-no-shift.json' import lowerTempDays from './fixtures/lower-temps.json' import firstException from './fixtures/first-exception-rule.json' +import firstExceptionNoShift from './fixtures/first-exception-rule-no-shift.json' const expect = chai.expect describe.only('sensiplan', () => { describe('getTemperatureStatus', () => { - it('reports lower temperature status before shift', function () { - const status = getTemperatureStatus('2018-06-09', lowerTempDays) - expect(status).to.eql({ - low: [36.7, 36.55, 36.45, 36.5, 36.55], - ltl: 36.7, - high: [], - shiftDetected: false + describe('regular rule', () => { + it('reports lower temperature status before shift', function () { + const status = getTemperatureStatus('2018-06-09', lowerTempDays) + expect(status).to.eql({ + low: [36.7, 36.55, 36.45, 36.5, 36.55], + ltl: 36.7, + high: [], + shiftDetected: false + }) + }) + + it('detects temperature shift correctly', function () { + const status = getTemperatureStatus('2018-06-14', tempShift) + expect(status).to.eql({ + low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], + ltl: 36.6, + high: [36.8, 36.85, 36.8], + shiftDetected: true + }) + }) + + it('detects missing temperature shift correctly', function () { + const status = getTemperatureStatus('2018-06-14', noTempShift) + expect(status).to.eql({ + low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], + ltl: 36.6, + high: [36.8, 36.85, 36.75], + shiftDetected: false + }) }) }) - it('detects temperature shift according to regular rule', function () { - const status = getTemperatureStatus('2018-06-14', tempShift) - expect(status).to.eql({ - low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], - ltl: 36.6, - high: [36.8, 36.85, 36.8], - shiftDetected: true + describe('1st exception rule', () => { + it('detects temperature shift', function () { + const status = getTemperatureStatus('2018-06-14', firstException) + expect(status).to.eql({ + low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], + ltl: 36.6, + high: [36.8, 36.85, 36.75, 36.65], + shiftDetected: true + }) }) - }) - it('detects temperature shift according to 1st exception rule', function () { - const status = getTemperatureStatus('2018-06-14', firstException) - expect(status).to.eql({ - low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], - ltl: 36.6, - high: [36.8, 36.85, 36.75, 36.65], - shiftDetected: true + it('detects missing temperature shift correctly', function () { + const status = getTemperatureStatus('2018-06-14', firstExceptionNoShift) + expect(status).to.eql({ + low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], + ltl: 36.6, + high: [36.8, 36.85, 36.75, 36.55], + shiftDetected: false + }) }) }) }) From d1f2922357c1655c7709305214a98d6851664346 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Sat, 30 Jun 2018 16:13:05 +0200 Subject: [PATCH 07/59] Change function signature and remove fixtures --- lib/sensiplan.js | 9 ++- .../first-exception-rule-no-shift.json | 67 ------------------- test/fixtures/first-exception-rule.json | 67 ------------------- test/fixtures/lower-temps.json | 31 --------- test/fixtures/regular-rule-no-shift.json | 61 ----------------- test/fixtures/regular-rule-shift.json | 61 ----------------- test/sensiplan.spec.js | 22 +++--- 7 files changed, 15 insertions(+), 303 deletions(-) delete mode 100644 test/fixtures/first-exception-rule-no-shift.json delete mode 100644 test/fixtures/first-exception-rule.json delete mode 100644 test/fixtures/lower-temps.json delete mode 100644 test/fixtures/regular-rule-no-shift.json delete mode 100644 test/fixtures/regular-rule-shift.json diff --git a/lib/sensiplan.js b/lib/sensiplan.js index 1364665..45e2469 100644 --- a/lib/sensiplan.js +++ b/lib/sensiplan.js @@ -1,7 +1,6 @@ -function getTemperatureStatus(targetDateString, previousDaysInCycle) { - const tempValues = previousDaysInCycle - .filter(day => day.temperature) - .map(day => !day.temperature.exclude && rounded(day.temperature.value, 0.05)) +function detectTemperatureShift(temperaturesOfCycle) { + // sensiplan rounds temps to the nearest 0.05 + const tempValues = temperaturesOfCycle.map(val => rounded(val, 0.05)) let detectingPotentialHighLevel = false @@ -49,5 +48,5 @@ function rounded(val, step) { } export { - getTemperatureStatus + detectTemperatureShift } \ No newline at end of file diff --git a/test/fixtures/first-exception-rule-no-shift.json b/test/fixtures/first-exception-rule-no-shift.json deleted file mode 100644 index 559aefb..0000000 --- a/test/fixtures/first-exception-rule-no-shift.json +++ /dev/null @@ -1,67 +0,0 @@ -[{ - "date": "2018-06-04", - "temperature": { - "value": 36.7, - "exclude": null - } -}, { - "date": "2018-06-05", - "temperature": { - "value": 36.57, - "exclude": null - } -}, { - "date": "2018-06-06", - "temperature": { - "value": 36.47, - "exclude": null - } -}, { - "date": "2018-06-07", - "temperature": { - "value": 36.49, - "exclude": null - } -}, { - "date": "2018-06-09", - "temperature": { - "value": 36.57, - "exclude": null - } -}, { - "date": "2018-06-10", - "temperature": { - "value": 36.62, - "exclude": null - } -}, { - "date": "2018-06-11", - "temperature": { - "value": 36.55, - "exclude": null - } -}, { - "date": "2018-06-12", - "temperature": { - "value": 36.8, - "exclude": null - } -}, { - "date": "2018-06-13", - "temperature": { - "value": 36.86, - "exclude": null - } -}, { - "date": "2018-06-14", - "temperature": { - "value": 36.77, - "exclude": null - } -}, { - "date": "2018-06-15", - "temperature": { - "value": 36.57, - "exclude": null - } -}] \ No newline at end of file diff --git a/test/fixtures/first-exception-rule.json b/test/fixtures/first-exception-rule.json deleted file mode 100644 index 2b963ef..0000000 --- a/test/fixtures/first-exception-rule.json +++ /dev/null @@ -1,67 +0,0 @@ -[{ - "date": "2018-06-04", - "temperature": { - "value": 36.7, - "exclude": null - } -}, { - "date": "2018-06-05", - "temperature": { - "value": 36.57, - "exclude": null - } -}, { - "date": "2018-06-06", - "temperature": { - "value": 36.47, - "exclude": null - } -}, { - "date": "2018-06-07", - "temperature": { - "value": 36.49, - "exclude": null - } -}, { - "date": "2018-06-09", - "temperature": { - "value": 36.57, - "exclude": null - } -}, { - "date": "2018-06-10", - "temperature": { - "value": 36.62, - "exclude": null - } -}, { - "date": "2018-06-11", - "temperature": { - "value": 36.55, - "exclude": null - } -}, { - "date": "2018-06-12", - "temperature": { - "value": 36.8, - "exclude": null - } -}, { - "date": "2018-06-13", - "temperature": { - "value": 36.86, - "exclude": null - } -}, { - "date": "2018-06-14", - "temperature": { - "value": 36.77, - "exclude": null - } -}, { - "date": "2018-06-15", - "temperature": { - "value": 36.63, - "exclude": null - } -}] \ No newline at end of file diff --git a/test/fixtures/lower-temps.json b/test/fixtures/lower-temps.json deleted file mode 100644 index 20ae088..0000000 --- a/test/fixtures/lower-temps.json +++ /dev/null @@ -1,31 +0,0 @@ -[{ - "date": "2018-06-04", - "temperature": { - "value": 36.7, - "exclude": null - } -}, { - "date": "2018-06-05", - "temperature": { - "value": 36.57, - "exclude": null - } -}, { - "date": "2018-06-06", - "temperature": { - "value": 36.47, - "exclude": null - } -}, { - "date": "2018-06-07", - "temperature": { - "value": 36.49, - "exclude": null - } -}, { - "date": "2018-06-09", - "temperature": { - "value": 36.57, - "exclude": null - } -}] \ No newline at end of file diff --git a/test/fixtures/regular-rule-no-shift.json b/test/fixtures/regular-rule-no-shift.json deleted file mode 100644 index ca99c5a..0000000 --- a/test/fixtures/regular-rule-no-shift.json +++ /dev/null @@ -1,61 +0,0 @@ -[{ - "date": "2018-06-04", - "temperature": { - "value": 36.7, - "exclude": null - } -}, { - "date": "2018-06-05", - "temperature": { - "value": 36.57, - "exclude": null - } -}, { - "date": "2018-06-06", - "temperature": { - "value": 36.47, - "exclude": null - } -}, { - "date": "2018-06-07", - "temperature": { - "value": 36.49, - "exclude": null - } -}, { - "date": "2018-06-09", - "temperature": { - "value": 36.57, - "exclude": null - } -}, { - "date": "2018-06-10", - "temperature": { - "value": 36.62, - "exclude": null - } -}, { - "date": "2018-06-11", - "temperature": { - "value": 36.55, - "exclude": null - } -}, { - "date": "2018-06-12", - "temperature": { - "value": 36.8, - "exclude": null - } -}, { - "date": "2018-06-13", - "temperature": { - "value": 36.86, - "exclude": null - } -}, { - "date": "2018-06-14", - "temperature": { - "value": 36.77, - "exclude": null - } -}] \ No newline at end of file diff --git a/test/fixtures/regular-rule-shift.json b/test/fixtures/regular-rule-shift.json deleted file mode 100644 index 74bd559..0000000 --- a/test/fixtures/regular-rule-shift.json +++ /dev/null @@ -1,61 +0,0 @@ -[{ - "date": "2018-06-04", - "temperature": { - "value": 36.7, - "exclude": null - } -}, { - "date": "2018-06-05", - "temperature": { - "value": 36.57, - "exclude": null - } -}, { - "date": "2018-06-06", - "temperature": { - "value": 36.47, - "exclude": null - } -}, { - "date": "2018-06-07", - "temperature": { - "value": 36.49, - "exclude": null - } -}, { - "date": "2018-06-09", - "temperature": { - "value": 36.57, - "exclude": null - } -}, { - "date": "2018-06-10", - "temperature": { - "value": 36.62, - "exclude": null - } -}, { - "date": "2018-06-11", - "temperature": { - "value": 36.55, - "exclude": null - } -}, { - "date": "2018-06-12", - "temperature": { - "value": 36.8, - "exclude": null - } -}, { - "date": "2018-06-13", - "temperature": { - "value": 36.86, - "exclude": null - } -}, { - "date": "2018-06-14", - "temperature": { - "value": 36.8, - "exclude": null - } -}] \ No newline at end of file diff --git a/test/sensiplan.spec.js b/test/sensiplan.spec.js index 055f6db..3f69d2a 100644 --- a/test/sensiplan.spec.js +++ b/test/sensiplan.spec.js @@ -1,10 +1,5 @@ import chai from 'chai' -import { getTemperatureStatus } from '../lib/sensiplan' -import tempShift from './fixtures/regular-rule-shift.json' -import noTempShift from './fixtures/regular-rule-no-shift.json' -import lowerTempDays from './fixtures/lower-temps.json' -import firstException from './fixtures/first-exception-rule.json' -import firstExceptionNoShift from './fixtures/first-exception-rule-no-shift.json' +import { detectTemperatureShift } from '../lib/sensiplan' const expect = chai.expect @@ -12,7 +7,8 @@ describe.only('sensiplan', () => { describe('getTemperatureStatus', () => { describe('regular rule', () => { it('reports lower temperature status before shift', function () { - const status = getTemperatureStatus('2018-06-09', lowerTempDays) + const lowerTemps = [36.7, 36.57, 36.47, 36.49, 36.57] + const status = detectTemperatureShift(lowerTemps) expect(status).to.eql({ low: [36.7, 36.55, 36.45, 36.5, 36.55], ltl: 36.7, @@ -22,7 +18,8 @@ describe.only('sensiplan', () => { }) it('detects temperature shift correctly', function () { - const status = getTemperatureStatus('2018-06-14', tempShift) + const tempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] + const status = detectTemperatureShift(tempShift) expect(status).to.eql({ low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, @@ -32,7 +29,8 @@ describe.only('sensiplan', () => { }) it('detects missing temperature shift correctly', function () { - const status = getTemperatureStatus('2018-06-14', noTempShift) + const noTempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] + const status = detectTemperatureShift(noTempShift) expect(status).to.eql({ low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, @@ -44,7 +42,8 @@ describe.only('sensiplan', () => { describe('1st exception rule', () => { it('detects temperature shift', function () { - const status = getTemperatureStatus('2018-06-14', firstException) + const firstException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77, 36.63] + const status = detectTemperatureShift(firstException) expect(status).to.eql({ low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, @@ -54,7 +53,8 @@ describe.only('sensiplan', () => { }) it('detects missing temperature shift correctly', function () { - const status = getTemperatureStatus('2018-06-14', firstExceptionNoShift) + const firstExceptionNoShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77, 36.57] + const status = detectTemperatureShift(firstExceptionNoShift) expect(status).to.eql({ low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, From 5e0601320b141295cf7fb57984ff4407985f2522 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Sat, 30 Jun 2018 16:39:46 +0200 Subject: [PATCH 08/59] Add test for less than 6 low temps --- lib/sensiplan.js | 2 +- test/sensiplan.spec.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/sensiplan.js b/lib/sensiplan.js index 45e2469..825df3e 100644 --- a/lib/sensiplan.js +++ b/lib/sensiplan.js @@ -9,7 +9,7 @@ function detectTemperatureShift(temperaturesOfCycle) { // if no shift has been detected, we collect low temps // after the shift has been detected, we count them as part // of the higher temperature phase - if (acc.low.length < 7) { + if (acc.low.length < 6) { acc.low.push(curr) acc.ltl = Math.max(...acc.low.slice(-6)) // TODO these are the same diff --git a/test/sensiplan.spec.js b/test/sensiplan.spec.js index 3f69d2a..e651ca8 100644 --- a/test/sensiplan.spec.js +++ b/test/sensiplan.spec.js @@ -28,6 +28,28 @@ describe.only('sensiplan', () => { }) }) + it('detects no temperature shift when there are no 6 low temps', function () { + const tempShift = [36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] + const status = detectTemperatureShift(tempShift) + expect(status).to.eql({ + low: [36.45, 36.5, 36.55, 36.6, 36.55, 36.8], + ltl: 36.8, + high: [36.85, 36.8], + shiftDetected: false + }) + }) + + it('detects no temperature shift if the shift is not high enough', function () { + const tempShift = [36.57, 36.7, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] + const status = detectTemperatureShift(tempShift) + expect(status).to.eql({ + low: [36.55, 36.7, 36.45, 36.5, 36.55, 36.6, 36.55], + ltl: 36.7, + high: [36.8, 36.85, 36.8], + shiftDetected: false + }) + }) + it('detects missing temperature shift correctly', function () { const noTempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] const status = detectTemperatureShift(noTempShift) From a90d393545a75626357b048d2c86a3683ebe587d Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Sat, 30 Jun 2018 17:59:26 +0200 Subject: [PATCH 09/59] Return potential high temps as well --- lib/sensiplan.js | 43 +++++++++++++++++++++++------------------- test/sensiplan.spec.js | 24 +++++++++++------------ 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/lib/sensiplan.js b/lib/sensiplan.js index 825df3e..94f7b43 100644 --- a/lib/sensiplan.js +++ b/lib/sensiplan.js @@ -2,8 +2,6 @@ function detectTemperatureShift(temperaturesOfCycle) { // sensiplan rounds temps to the nearest 0.05 const tempValues = temperaturesOfCycle.map(val => rounded(val, 0.05)) - let detectingPotentialHighLevel = false - return tempValues.reduce((acc, curr) => { // if we don't yet have 6 lower temps, we just collect // if no shift has been detected, we collect low temps @@ -11,32 +9,23 @@ function detectTemperatureShift(temperaturesOfCycle) { // of the higher temperature phase if (acc.low.length < 6) { acc.low.push(curr) - acc.ltl = Math.max(...acc.low.slice(-6)) + acc.ltl = Math.max(...acc.low) // TODO these are the same - } else if (curr <= acc.ltl && !detectingPotentialHighLevel && !acc.shiftDetected) { + } else if (curr <= acc.ltl && !acc.potentialHigh && !acc.shiftDetected) { acc.low.push(curr) - acc.ltl = Math.max(...acc.low.slice(-6)) + acc.low.shift(curr) + acc.ltl = Math.max(...acc.low) + } else if (!acc.shiftDetected){ + if (!acc.potentialHigh) acc.potentialHigh = [] + acc.potentialHigh.push(curr) + checkRules(acc, curr) } else { - detectingPotentialHighLevel = true acc.high.push(curr) } - // regular rule - // we round the difference because of JS decimal weirdness - if (acc.high.length === 3 && rounded(curr - acc.ltl, 0.01) >= 0.2) { - acc.shiftDetected = true - detectingPotentialHighLevel = false - } - // 1st exception rule - if (acc.high.length === 4 && curr > acc.ltl) { - acc.shiftDetected = true - detectingPotentialHighLevel = false - } - return acc }, { low: [], - high: [], ltl: null, shiftDetected: false }) @@ -47,6 +36,22 @@ function rounded(val, step) { return Math.round(val * inverted) / inverted } +function checkRules(acc, curr) { + function regularRuleApplies() { + // we round the difference because of JS decimal weirdness + return acc.potentialHigh.length === 3 && rounded(curr - acc.ltl, 0.01) >= 0.2 + } + function firstExceptionRuleApplies() { + return acc.potentialHigh.length === 4 && curr > acc.ltl + } + + if (regularRuleApplies() || firstExceptionRuleApplies()) { + acc.shiftDetected = true + acc.high = acc.potentialHigh + delete acc.potentialHigh + } +} + export { detectTemperatureShift } \ No newline at end of file diff --git a/test/sensiplan.spec.js b/test/sensiplan.spec.js index e651ca8..dd9b583 100644 --- a/test/sensiplan.spec.js +++ b/test/sensiplan.spec.js @@ -12,8 +12,7 @@ describe.only('sensiplan', () => { expect(status).to.eql({ low: [36.7, 36.55, 36.45, 36.5, 36.55], ltl: 36.7, - high: [], - shiftDetected: false + shiftDetected: false, }) }) @@ -21,7 +20,7 @@ describe.only('sensiplan', () => { const tempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] const status = detectTemperatureShift(tempShift) expect(status).to.eql({ - low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], + low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, high: [36.8, 36.85, 36.8], shiftDetected: true @@ -34,7 +33,7 @@ describe.only('sensiplan', () => { expect(status).to.eql({ low: [36.45, 36.5, 36.55, 36.6, 36.55, 36.8], ltl: 36.8, - high: [36.85, 36.8], + potentialHigh: [36.85, 36.8], shiftDetected: false }) }) @@ -43,9 +42,9 @@ describe.only('sensiplan', () => { const tempShift = [36.57, 36.7, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] const status = detectTemperatureShift(tempShift) expect(status).to.eql({ - low: [36.55, 36.7, 36.45, 36.5, 36.55, 36.6, 36.55], + low: [36.7, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.7, - high: [36.8, 36.85, 36.8], + potentialHigh: [36.8, 36.85, 36.8], shiftDetected: false }) }) @@ -54,9 +53,9 @@ describe.only('sensiplan', () => { const noTempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] const status = detectTemperatureShift(noTempShift) expect(status).to.eql({ - low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], + low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, - high: [36.8, 36.85, 36.75], + potentialHigh: [36.8, 36.85, 36.75], shiftDetected: false }) }) @@ -67,20 +66,19 @@ describe.only('sensiplan', () => { const firstException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77, 36.63] const status = detectTemperatureShift(firstException) expect(status).to.eql({ - low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], + low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, high: [36.8, 36.85, 36.75, 36.65], shiftDetected: true }) }) - it('detects missing temperature shift correctly', function () { + it.skip('detects missing temperature shift correctly', function () { const firstExceptionNoShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77, 36.57] const status = detectTemperatureShift(firstExceptionNoShift) expect(status).to.eql({ - low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55], - ltl: 36.6, - high: [36.8, 36.85, 36.75, 36.55], + low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55, 36.8, 36.85, 36.75, 36.55], + ltl: 36.85, shiftDetected: false }) }) From 1eb3f4a14a3ad616dd8bfddc3c1d6039dc8d73a6 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Sat, 30 Jun 2018 20:28:48 +0200 Subject: [PATCH 10/59] Change API so it doesn't return unnecessary info --- lib/sensiplan.js | 105 ++++++++++++++++++++++++++++------------- test/sensiplan.spec.js | 43 +++++------------ 2 files changed, 84 insertions(+), 64 deletions(-) diff --git a/lib/sensiplan.js b/lib/sensiplan.js index 94f7b43..8a1e7cd 100644 --- a/lib/sensiplan.js +++ b/lib/sensiplan.js @@ -2,32 +2,42 @@ function detectTemperatureShift(temperaturesOfCycle) { // sensiplan rounds temps to the nearest 0.05 const tempValues = temperaturesOfCycle.map(val => rounded(val, 0.05)) - return tempValues.reduce((acc, curr) => { - // if we don't yet have 6 lower temps, we just collect - // if no shift has been detected, we collect low temps - // after the shift has been detected, we count them as part - // of the higher temperature phase - if (acc.low.length < 6) { - acc.low.push(curr) - acc.ltl = Math.max(...acc.low) - // TODO these are the same - } else if (curr <= acc.ltl && !acc.potentialHigh && !acc.shiftDetected) { - acc.low.push(curr) - acc.low.shift(curr) - acc.ltl = Math.max(...acc.low) - } else if (!acc.shiftDetected){ - if (!acc.potentialHigh) acc.potentialHigh = [] - acc.potentialHigh.push(curr) - checkRules(acc, curr) - } else { - acc.high.push(curr) + function getLtl(i) { + const sixTempsBefore = getSixTempsBefore(i) + return Math.max(...sixTempsBefore) + } + function getSixTempsBefore(i) { + return tempValues.slice(0, i).slice(-6) + } + + return tempValues.reduce((acc, temp, i) => { + // need at least 6 low temps before we can detect a first high measurement + if (i < 6) return acc + + // if we've already detected a shift, we put it with the other high level temps + if(acc.detected) { + acc.high.push(temp) + return acc } + // is the temp a candidate for a first high measurement? + const ltl = getLtl(i) + if (temp <= ltl) return acc + + const checkResult = checkIfFirstHighMeasurement(temp, i, tempValues, ltl) + // if we don't have a winner, keep going + if (!checkResult.isFirstHighMeasurement) return acc + + // if we do, remember the details and start collecting the high level temps + acc.detected = true + acc.high = [temp] + acc.rules = checkResult.rules + acc.ltl = ltl + acc.low = getSixTempsBefore(i) + return acc }, { - low: [], - ltl: null, - shiftDetected: false + detected: false }) } @@ -36,20 +46,51 @@ function rounded(val, step) { return Math.round(val * inverted) / inverted } -function checkRules(acc, curr) { - function regularRuleApplies() { - // we round the difference because of JS decimal weirdness - return acc.potentialHigh.length === 3 && rounded(curr - acc.ltl, 0.01) >= 0.2 +function checkIfFirstHighMeasurement(temp, i, temps, ltl) { + // need at least 3 high temps to form a high temperature level + if (i > temps.length - 3) { + return { isFirstHighMeasurement: false } } - function firstExceptionRuleApplies() { - return acc.potentialHigh.length === 4 && curr > acc.ltl + const nextTemps = temps.slice(i + 1, i + 4) + + if (regularRuleApplies(temp, nextTemps, ltl)) { + return { + isFirstHighMeasurement: true, + rules: { + regular: true, + }, + ltl + } } - if (regularRuleApplies() || firstExceptionRuleApplies()) { - acc.shiftDetected = true - acc.high = acc.potentialHigh - delete acc.potentialHigh + if (firstExceptionRuleApplies(temp, nextTemps, ltl)) { + return { + isFirstHighMeasurement: true, + rules: { + firstException: true, + }, + ltl + } } + + return { + isFirstHighMeasurement: false + } +} + +function regularRuleApplies(temp, nextTemps, ltl) { + if (!nextTemps.every(temp => temp > ltl)) return false + const thirdTemp = nextTemps[1] + // we round the difference because of JS decimal weirdness + if (rounded(thirdTemp - ltl, 0.1) < 0.2) return false + return true +} + +function firstExceptionRuleApplies(temp, nextTemps, ltl) { + if (!nextTemps.every(temp => temp > ltl)) return false + const fourthTemp = nextTemps[2] + if (fourthTemp > ltl) return true + return false } export { diff --git a/test/sensiplan.spec.js b/test/sensiplan.spec.js index dd9b583..51945ed 100644 --- a/test/sensiplan.spec.js +++ b/test/sensiplan.spec.js @@ -4,16 +4,12 @@ import { detectTemperatureShift } from '../lib/sensiplan' const expect = chai.expect describe.only('sensiplan', () => { - describe('getTemperatureStatus', () => { + describe('detect temperature shift', () => { describe('regular rule', () => { it('reports lower temperature status before shift', function () { const lowerTemps = [36.7, 36.57, 36.47, 36.49, 36.57] const status = detectTemperatureShift(lowerTemps) - expect(status).to.eql({ - low: [36.7, 36.55, 36.45, 36.5, 36.55], - ltl: 36.7, - shiftDetected: false, - }) + expect(status).to.eql({ detected: false }) }) it('detects temperature shift correctly', function () { @@ -23,41 +19,27 @@ describe.only('sensiplan', () => { low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, high: [36.8, 36.85, 36.8], - shiftDetected: true + detected: true, + rules: { regular: true } }) }) it('detects no temperature shift when there are no 6 low temps', function () { const tempShift = [36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] const status = detectTemperatureShift(tempShift) - expect(status).to.eql({ - low: [36.45, 36.5, 36.55, 36.6, 36.55, 36.8], - ltl: 36.8, - potentialHigh: [36.85, 36.8], - shiftDetected: false - }) + expect(status).to.eql({ detected: false }) }) it('detects no temperature shift if the shift is not high enough', function () { const tempShift = [36.57, 36.7, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] const status = detectTemperatureShift(tempShift) - expect(status).to.eql({ - low: [36.7, 36.45, 36.5, 36.55, 36.6, 36.55], - ltl: 36.7, - potentialHigh: [36.8, 36.85, 36.8], - shiftDetected: false - }) + expect(status).to.eql({ detected: false }) }) it('detects missing temperature shift correctly', function () { const noTempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] const status = detectTemperatureShift(noTempShift) - expect(status).to.eql({ - low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], - ltl: 36.6, - potentialHigh: [36.8, 36.85, 36.75], - shiftDetected: false - }) + expect(status).to.eql({ detected: false }) }) }) @@ -69,18 +51,15 @@ describe.only('sensiplan', () => { low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, high: [36.8, 36.85, 36.75, 36.65], - shiftDetected: true + detected: true, + rules: { firstException: true } }) }) - it.skip('detects missing temperature shift correctly', function () { + it('detects missing temperature shift correctly', function () { const firstExceptionNoShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77, 36.57] const status = detectTemperatureShift(firstExceptionNoShift) - expect(status).to.eql({ - low: [36.7, 36.55, 36.45, 36.5, 36.55, 36.6, 36.55, 36.8, 36.85, 36.75, 36.55], - ltl: 36.85, - shiftDetected: false - }) + expect(status).to.eql({ detected: false }) }) }) }) From 8db92e96f3197ba7d19bbe29c61870661d4963f9 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Mon, 2 Jul 2018 12:46:16 +0200 Subject: [PATCH 11/59] Add tests for invalid shifts --- test/sensiplan.spec.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/sensiplan.spec.js b/test/sensiplan.spec.js index 51945ed..bb3719c 100644 --- a/test/sensiplan.spec.js +++ b/test/sensiplan.spec.js @@ -41,6 +41,26 @@ describe.only('sensiplan', () => { const status = detectTemperatureShift(noTempShift) expect(status).to.eql({ detected: false }) }) + + it('detects shift after an earlier one was invalid', function () { + const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.7, 36.8, 36.9] + + const status = detectTemperatureShift(temps) + expect(status).to.eql({ + low: [36.4, 36.4, 36.6, 36.6, 36.4, 36.4], + ltl: 36.6, + high: [36.7, 36.8, 36.9], + detected: true, + rules: { regular: true } + }) + }) + + it('detects 2 consecutive invalid shifts', function () { + const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.6, 36.6, 36.7] + + const status = detectTemperatureShift(temps) + expect(status).to.eql({ detected: false }) + }) }) describe('1st exception rule', () => { @@ -61,6 +81,19 @@ describe.only('sensiplan', () => { const status = detectTemperatureShift(firstExceptionNoShift) expect(status).to.eql({ detected: false }) }) + + it('detects shift after an earlier one was invalid', function () { + const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.7, 36.7, 36.7, 36.7] + + const status = detectTemperatureShift(temps) + expect(status).to.eql({ + low: [36.4, 36.4, 36.6, 36.6, 36.4, 36.4], + ltl: 36.6, + high: [36.7, 36.7, 36.7, 36.7], + detected: true, + rules: { firstException: true } + }) + }) }) }) }) \ No newline at end of file From 03a0f1fb754b573358847cbdf3f9c07f509f52ff Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Mon, 2 Jul 2018 13:37:11 +0200 Subject: [PATCH 12/59] Implement second exception rule --- lib/sensiplan.js | 32 +++++++++++++++++++++++- test/sensiplan.spec.js | 56 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/lib/sensiplan.js b/lib/sensiplan.js index 8a1e7cd..ed36953 100644 --- a/lib/sensiplan.js +++ b/lib/sensiplan.js @@ -42,6 +42,7 @@ function detectTemperatureShift(temperaturesOfCycle) { } function rounded(val, step) { + // we round the difference because of JS decimal weirdness const inverted = 1 / step return Math.round(val * inverted) / inverted } @@ -73,6 +74,16 @@ function checkIfFirstHighMeasurement(temp, i, temps, ltl) { } } + if (secondExceptionRuleApplies(temp, nextTemps, ltl)) { + return { + isFirstHighMeasurement: true, + rules: { + secondException: true, + }, + ltl + } + } + return { isFirstHighMeasurement: false } @@ -81,18 +92,37 @@ function checkIfFirstHighMeasurement(temp, i, temps, ltl) { function regularRuleApplies(temp, nextTemps, ltl) { if (!nextTemps.every(temp => temp > ltl)) return false const thirdTemp = nextTemps[1] - // we round the difference because of JS decimal weirdness if (rounded(thirdTemp - ltl, 0.1) < 0.2) return false return true } function firstExceptionRuleApplies(temp, nextTemps, ltl) { + if (nextTemps.length < 3) return false if (!nextTemps.every(temp => temp > ltl)) return false const fourthTemp = nextTemps[2] if (fourthTemp > ltl) return true return false } +function secondExceptionRuleApplies(temp, nextTemps, ltl) { + if (nextTemps.length < 3) return false + if (secondOrThirdTempIsAtOrBelowLtl(nextTemps, ltl)) { + const fourthTemp = nextTemps[2] + if (rounded(fourthTemp - ltl, 0.1) >= 0.2) return true + } + return false +} + +function secondOrThirdTempIsAtOrBelowLtl(nextTemps, ltl) { + const secondIsLow = nextTemps[0] <= ltl + const thirdIsLow = nextTemps[1] <= ltl + if ((secondIsLow || thirdIsLow) && !(secondIsLow && thirdIsLow)) { + return true + } else { + return false + } +} + export { detectTemperatureShift } \ No newline at end of file diff --git a/test/sensiplan.spec.js b/test/sensiplan.spec.js index bb3719c..f490a86 100644 --- a/test/sensiplan.spec.js +++ b/test/sensiplan.spec.js @@ -82,6 +82,12 @@ describe.only('sensiplan', () => { expect(status).to.eql({ detected: false }) }) + it('detects missing temperature shift with not enough high temps', function () { + const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] + const status = detectTemperatureShift(temps) + expect(status).to.eql({ detected: false }) + }) + it('detects shift after an earlier one was invalid', function () { const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.7, 36.7, 36.7, 36.7] @@ -95,5 +101,55 @@ describe.only('sensiplan', () => { }) }) }) + + describe('2nd exception rule', () => { + it('detects temperature shift with exception temp eql ltl', function () { + const secondException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.6, 36.8] + const status = detectTemperatureShift(secondException) + expect(status).to.eql({ + low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], + ltl: 36.6, + high: [36.8, 36.85, 36.6, 36.8], + detected: true, + rules: { secondException: true } + }) + }) + + it('detects temperature shift with exception temp lower than ltl', function () { + const secondException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.8] + const status = detectTemperatureShift(secondException) + expect(status).to.eql({ + low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], + ltl: 36.6, + high: [36.8, 36.85, 36.4, 36.8], + detected: true, + rules: { secondException: true } + }) + }) + + it('detects missing temperature shift correctly', function () { + const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.77, 36.77] + const status = detectTemperatureShift(temps) + expect(status).to.eql({ detected: false }) + }) + + it('detects missing temperature shift when not enough high temps', function () { + const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4] + const status = detectTemperatureShift(temps) + expect(status).to.eql({ detected: false }) + }) + + it('detects shift after an earlier one was invalid', function () { + const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.77, 36.9, 36.9, 36.86, 37.04] + const status = detectTemperatureShift(temps) + expect(status).to.eql({ + low: [36.6, 36.55, 36.8, 36.85, 36.4, 36.75], + ltl: 36.85, + high: [36.9, 36.9, 36.85, 37.05], + detected: true, + rules: { secondException: true } + }) + }) + }) }) }) \ No newline at end of file From 1c791df5ad84bb88674ca99d156fdb93ca189563 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Mon, 2 Jul 2018 16:55:20 +0200 Subject: [PATCH 13/59] Hook up fertility temp status to app --- components/cycle-day-overview.js | 2 -- components/cycle-day.js | 3 +++ lib/cycle.js | 26 ++++++++++++++++++-------- lib/sensiplan-adapter.js | 29 +++++++++++++++++++++++++++++ lib/sensiplan.js | 14 ++++---------- styles/index.js | 8 +++----- 6 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 lib/sensiplan-adapter.js diff --git a/components/cycle-day-overview.js b/components/cycle-day-overview.js index f8c3aca..49068fe 100644 --- a/components/cycle-day-overview.js +++ b/components/cycle-day-overview.js @@ -14,8 +14,6 @@ const getCycleDayNumber = cycleModule().getCycleDayNumber export default class DayView extends Component { constructor(props) { super(props) - console.log('new') - console.log(props.cycleDay) this.cycleDay = props.cycleDay this.showView = props.showView this.state = { diff --git a/components/cycle-day.js b/components/cycle-day.js index 2d78220..5b54b2f 100644 --- a/components/cycle-day.js +++ b/components/cycle-day.js @@ -4,6 +4,7 @@ import { Text } from 'react-native' import cycleModule from '../lib/cycle' +import { getTemperatureFertilityStatus } from '../lib/sensiplan-adapter' import DayView from './cycle-day-overview' import BleedingEditView from './bleeding' import TemperatureEditView from './temperature' @@ -28,6 +29,7 @@ export default class Day extends Component { render() { const cycleDayNumber = getCycleDayNumber(this.cycleDay.date) + const temperatureFertilityStatus = getTemperatureFertilityStatus(this.cycleDay.date) return ( @@ -37,6 +39,7 @@ export default class Day extends Component { { cycleDayNumber && Cycle day {cycleDayNumber} } + { cycleDayNumber && Temperature status: {temperatureFertilityStatus} } { diff --git a/lib/cycle.js b/lib/cycle.js index 8dbe838..ec33427 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -1,16 +1,21 @@ import * as joda from 'js-joda' - const LocalDate = joda.LocalDate -export default function config(opts = {}) { +export default function config(opts) { let bleedingDaysSortedByDate - if (!opts.bleedingDaysSortedByDate) { + let temperatureDaysSortedByDate + let maxBreakInBleeding + + if (!opts) { // we only want to require (and run) the db module when not running the tests bleedingDaysSortedByDate = require('../db').bleedingDaysSortedByDate + temperatureDaysSortedByDate = require('../db').temperatureDaysSortedByDate + maxBreakInBleeding = 1 } else { - bleedingDaysSortedByDate = opts.bleedingDaysSortedByDate + bleedingDaysSortedByDate = opts.bleedingDaysSortedByDate || [] + temperatureDaysSortedByDate = opts.temperatureDaysSortedByDate || [] + maxBreakInBleeding = opts.maxBreakInBleeding || 1 } - const maxBreakInBleeding = opts.maxBreakInBleeding || 1 function getLastMensesStart(targetDateString) { const targetDate = LocalDate.parse(targetDateString) @@ -53,13 +58,18 @@ export default function config(opts = {}) { return diffInDays + 1 } - function getPreviousDaysInCycle() { - return [] + function getPreviousTemperaturesInCycle(targetDateString, lastMensesStart) { + const startIndex = temperatureDaysSortedByDate.findIndex(day => day.date <= targetDateString) + const previousTemperaturesInCycle = temperatureDaysSortedByDate.slice(startIndex) + const endIndex = previousTemperaturesInCycle.findIndex(day => day.date < lastMensesStart.date) + return previousTemperaturesInCycle + .slice(0, endIndex) + .map(day => day.temperature.value) } return { getCycleDayNumber, getLastMensesStart, - getPreviousDaysInCycle + getPreviousTemperaturesInCycle } } diff --git a/lib/sensiplan-adapter.js b/lib/sensiplan-adapter.js new file mode 100644 index 0000000..7ae60b3 --- /dev/null +++ b/lib/sensiplan-adapter.js @@ -0,0 +1,29 @@ +import { detectTemperatureShift } from './sensiplan' +import cycleModule from './cycle' + +const getLastMensesStart = cycleModule().getLastMensesStart +const getPreviousTemperaturesInCycle = cycleModule().getPreviousTemperaturesInCycle + +function getTemperatureFertilityStatus(targetDateString) { + const lastMensesStart = getLastMensesStart(targetDateString) + if (!lastMensesStart) return formatStatusForApp({ detected: false }) + const previousTemperaturesInCycle = getPreviousTemperaturesInCycle(targetDateString, lastMensesStart) + // we get temps with latest first, but sensiplan module expects latest last + previousTemperaturesInCycle.reverse() + const status = detectTemperatureShift(previousTemperaturesInCycle) + return formatStatusForApp(status) +} + +function formatStatusForApp(status) { + if (!status.detected) return 'fertile' + const dict = [ + "regular temperature", + "first exception", + "second exception" + ] + return `infertile according to the ${dict[status.rule]} rule` +} + +export { + getTemperatureFertilityStatus +} \ No newline at end of file diff --git a/lib/sensiplan.js b/lib/sensiplan.js index ed36953..3f66c47 100644 --- a/lib/sensiplan.js +++ b/lib/sensiplan.js @@ -31,7 +31,7 @@ function detectTemperatureShift(temperaturesOfCycle) { // if we do, remember the details and start collecting the high level temps acc.detected = true acc.high = [temp] - acc.rules = checkResult.rules + acc.rule = checkResult.rule acc.ltl = ltl acc.low = getSixTempsBefore(i) @@ -57,9 +57,7 @@ function checkIfFirstHighMeasurement(temp, i, temps, ltl) { if (regularRuleApplies(temp, nextTemps, ltl)) { return { isFirstHighMeasurement: true, - rules: { - regular: true, - }, + rule: 0, ltl } } @@ -67,9 +65,7 @@ function checkIfFirstHighMeasurement(temp, i, temps, ltl) { if (firstExceptionRuleApplies(temp, nextTemps, ltl)) { return { isFirstHighMeasurement: true, - rules: { - firstException: true, - }, + rule: 1, ltl } } @@ -77,9 +73,7 @@ function checkIfFirstHighMeasurement(temp, i, temps, ltl) { if (secondExceptionRuleApplies(temp, nextTemps, ltl)) { return { isFirstHighMeasurement: true, - rules: { - secondException: true, - }, + rule: 2, ltl } } diff --git a/styles/index.js b/styles/index.js index 5eb9cc1..204e132 100644 --- a/styles/index.js +++ b/styles/index.js @@ -14,14 +14,13 @@ export default StyleSheet.create({ dateHeader: { fontSize: 20, fontWeight: 'bold', - margin: 30, + margin: 20, color: 'white', textAlign: 'center', textAlignVertical: 'center' }, cycleDayNumber: { fontSize: 18, - margin: 20, textAlign: 'center', textAlignVertical: 'center' }, @@ -77,14 +76,13 @@ export default StyleSheet.create({ justifyContent: 'space-around' }, cycleDayDateView: { - flex: 2, justifyContent: 'center', backgroundColor: 'steelblue' }, cycleDayNumberView: { - flex: 1, justifyContent: 'center', - backgroundColor: 'skyblue' + backgroundColor: 'skyblue', + padding: 10 }, cycleDaySymptomsView: { flex: 8, From 9c1876e87696970066ce846fd92606fdff49acf0 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 3 Jul 2018 09:19:06 +0200 Subject: [PATCH 14/59] Change sympto module structure --- components/cycle-day.js | 2 +- lib/{sensiplan-adapter.js => sympto-adapter.js} | 4 ++-- lib/{sensiplan.js => sympto/temperature.js} | 0 test/{sensiplan.spec.js => sympto/temperature.spec.js} | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename lib/{sensiplan-adapter.js => sympto-adapter.js} (86%) rename lib/{sensiplan.js => sympto/temperature.js} (100%) rename test/{sensiplan.spec.js => sympto/temperature.spec.js} (98%) diff --git a/components/cycle-day.js b/components/cycle-day.js index 5b54b2f..aafb099 100644 --- a/components/cycle-day.js +++ b/components/cycle-day.js @@ -4,7 +4,7 @@ import { Text } from 'react-native' import cycleModule from '../lib/cycle' -import { getTemperatureFertilityStatus } from '../lib/sensiplan-adapter' +import { getTemperatureFertilityStatus } from '../lib/sympto-adapter' import DayView from './cycle-day-overview' import BleedingEditView from './bleeding' import TemperatureEditView from './temperature' diff --git a/lib/sensiplan-adapter.js b/lib/sympto-adapter.js similarity index 86% rename from lib/sensiplan-adapter.js rename to lib/sympto-adapter.js index 7ae60b3..12f0935 100644 --- a/lib/sensiplan-adapter.js +++ b/lib/sympto-adapter.js @@ -1,4 +1,4 @@ -import { detectTemperatureShift } from './sensiplan' +import { detectTemperatureShift } from './sympto/temperature' import cycleModule from './cycle' const getLastMensesStart = cycleModule().getLastMensesStart @@ -8,7 +8,7 @@ function getTemperatureFertilityStatus(targetDateString) { const lastMensesStart = getLastMensesStart(targetDateString) if (!lastMensesStart) return formatStatusForApp({ detected: false }) const previousTemperaturesInCycle = getPreviousTemperaturesInCycle(targetDateString, lastMensesStart) - // we get temps with latest first, but sensiplan module expects latest last + // we get temps with latest first, but sympto module expects latest last previousTemperaturesInCycle.reverse() const status = detectTemperatureShift(previousTemperaturesInCycle) return formatStatusForApp(status) diff --git a/lib/sensiplan.js b/lib/sympto/temperature.js similarity index 100% rename from lib/sensiplan.js rename to lib/sympto/temperature.js diff --git a/test/sensiplan.spec.js b/test/sympto/temperature.spec.js similarity index 98% rename from test/sensiplan.spec.js rename to test/sympto/temperature.spec.js index f490a86..8646125 100644 --- a/test/sensiplan.spec.js +++ b/test/sympto/temperature.spec.js @@ -1,9 +1,9 @@ import chai from 'chai' -import { detectTemperatureShift } from '../lib/sensiplan' +import { detectTemperatureShift } from '../../lib/sympto/temperature' const expect = chai.expect -describe.only('sensiplan', () => { +describe.only('sympto', () => { describe('detect temperature shift', () => { describe('regular rule', () => { it('reports lower temperature status before shift', function () { From fb990b5a4b6e79f21129cb31751100845e2172f9 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 3 Jul 2018 09:22:04 +0200 Subject: [PATCH 15/59] Fix tests to fit new sympto api --- test/sympto/temperature.spec.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/sympto/temperature.spec.js b/test/sympto/temperature.spec.js index 8646125..c500a8e 100644 --- a/test/sympto/temperature.spec.js +++ b/test/sympto/temperature.spec.js @@ -20,7 +20,7 @@ describe.only('sympto', () => { ltl: 36.6, high: [36.8, 36.85, 36.8], detected: true, - rules: { regular: true } + rule: 0 }) }) @@ -51,7 +51,7 @@ describe.only('sympto', () => { ltl: 36.6, high: [36.7, 36.8, 36.9], detected: true, - rules: { regular: true } + rule: 0 }) }) @@ -72,7 +72,7 @@ describe.only('sympto', () => { ltl: 36.6, high: [36.8, 36.85, 36.75, 36.65], detected: true, - rules: { firstException: true } + rule: 1 }) }) @@ -97,7 +97,7 @@ describe.only('sympto', () => { ltl: 36.6, high: [36.7, 36.7, 36.7, 36.7], detected: true, - rules: { firstException: true } + rule: 1 }) }) }) @@ -111,7 +111,7 @@ describe.only('sympto', () => { ltl: 36.6, high: [36.8, 36.85, 36.6, 36.8], detected: true, - rules: { secondException: true } + rule: 2 }) }) @@ -123,7 +123,7 @@ describe.only('sympto', () => { ltl: 36.6, high: [36.8, 36.85, 36.4, 36.8], detected: true, - rules: { secondException: true } + rule: 2 }) }) @@ -147,7 +147,7 @@ describe.only('sympto', () => { ltl: 36.85, high: [36.9, 36.9, 36.85, 37.05], detected: true, - rules: { secondException: true } + rule: 2 }) }) }) From e4bae38d137d8a9585dc54f3a305dc2d7d8c6d44 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 3 Jul 2018 09:42:26 +0200 Subject: [PATCH 16/59] Introduce facade for sympto --- lib/sympto-adapter.js | 4 ++-- lib/sympto/index.js | 7 +++++++ lib/sympto/temperature.js | 8 ++------ test/sympto/temperature.spec.js | 34 ++++++++++++++++----------------- 4 files changed, 28 insertions(+), 25 deletions(-) create mode 100644 lib/sympto/index.js diff --git a/lib/sympto-adapter.js b/lib/sympto-adapter.js index 12f0935..d2f1ffb 100644 --- a/lib/sympto-adapter.js +++ b/lib/sympto-adapter.js @@ -1,4 +1,4 @@ -import { detectTemperatureShift } from './sympto/temperature' +import getTemperatureStatus from './sympto/temperature' import cycleModule from './cycle' const getLastMensesStart = cycleModule().getLastMensesStart @@ -10,7 +10,7 @@ function getTemperatureFertilityStatus(targetDateString) { const previousTemperaturesInCycle = getPreviousTemperaturesInCycle(targetDateString, lastMensesStart) // we get temps with latest first, but sympto module expects latest last previousTemperaturesInCycle.reverse() - const status = detectTemperatureShift(previousTemperaturesInCycle) + const status = getTemperatureStatus(previousTemperaturesInCycle) return formatStatusForApp(status) } diff --git a/lib/sympto/index.js b/lib/sympto/index.js new file mode 100644 index 0000000..92f513d --- /dev/null +++ b/lib/sympto/index.js @@ -0,0 +1,7 @@ +import getTemperatureStatus from './temperature' +import getMucusStatus from './mucus' + +export default function (cycleDays) { + const temperatureStatus = getTemperatureStatus(cycleDays) + const mucusStatus = getMucusStatus(cycleDays) +} \ No newline at end of file diff --git a/lib/sympto/temperature.js b/lib/sympto/temperature.js index 3f66c47..86a83be 100644 --- a/lib/sympto/temperature.js +++ b/lib/sympto/temperature.js @@ -1,4 +1,4 @@ -function detectTemperatureShift(temperaturesOfCycle) { +export default function getTemperatureStatus(temperaturesOfCycle) { // sensiplan rounds temps to the nearest 0.05 const tempValues = temperaturesOfCycle.map(val => rounded(val, 0.05)) @@ -25,7 +25,7 @@ function detectTemperatureShift(temperaturesOfCycle) { if (temp <= ltl) return acc const checkResult = checkIfFirstHighMeasurement(temp, i, tempValues, ltl) - // if we don't have a winner, keep going + // if we don't have a winner, keep move on to the next candidates if (!checkResult.isFirstHighMeasurement) return acc // if we do, remember the details and start collecting the high level temps @@ -115,8 +115,4 @@ function secondOrThirdTempIsAtOrBelowLtl(nextTemps, ltl) { } else { return false } -} - -export { - detectTemperatureShift } \ No newline at end of file diff --git a/test/sympto/temperature.spec.js b/test/sympto/temperature.spec.js index c500a8e..8106021 100644 --- a/test/sympto/temperature.spec.js +++ b/test/sympto/temperature.spec.js @@ -1,5 +1,5 @@ import chai from 'chai' -import { detectTemperatureShift } from '../../lib/sympto/temperature' +import getTemperatureStatus from '../../lib/sympto/temperature' const expect = chai.expect @@ -8,13 +8,13 @@ describe.only('sympto', () => { describe('regular rule', () => { it('reports lower temperature status before shift', function () { const lowerTemps = [36.7, 36.57, 36.47, 36.49, 36.57] - const status = detectTemperatureShift(lowerTemps) + const status = getTemperatureStatus(lowerTemps) expect(status).to.eql({ detected: false }) }) it('detects temperature shift correctly', function () { const tempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] - const status = detectTemperatureShift(tempShift) + const status = getTemperatureStatus(tempShift) expect(status).to.eql({ low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, @@ -26,26 +26,26 @@ describe.only('sympto', () => { it('detects no temperature shift when there are no 6 low temps', function () { const tempShift = [36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] - const status = detectTemperatureShift(tempShift) + const status = getTemperatureStatus(tempShift) expect(status).to.eql({ detected: false }) }) it('detects no temperature shift if the shift is not high enough', function () { const tempShift = [36.57, 36.7, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] - const status = detectTemperatureShift(tempShift) + const status = getTemperatureStatus(tempShift) expect(status).to.eql({ detected: false }) }) it('detects missing temperature shift correctly', function () { const noTempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] - const status = detectTemperatureShift(noTempShift) + const status = getTemperatureStatus(noTempShift) expect(status).to.eql({ detected: false }) }) it('detects shift after an earlier one was invalid', function () { const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.7, 36.8, 36.9] - const status = detectTemperatureShift(temps) + const status = getTemperatureStatus(temps) expect(status).to.eql({ low: [36.4, 36.4, 36.6, 36.6, 36.4, 36.4], ltl: 36.6, @@ -58,7 +58,7 @@ describe.only('sympto', () => { it('detects 2 consecutive invalid shifts', function () { const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.6, 36.6, 36.7] - const status = detectTemperatureShift(temps) + const status = getTemperatureStatus(temps) expect(status).to.eql({ detected: false }) }) }) @@ -66,7 +66,7 @@ describe.only('sympto', () => { describe('1st exception rule', () => { it('detects temperature shift', function () { const firstException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77, 36.63] - const status = detectTemperatureShift(firstException) + const status = getTemperatureStatus(firstException) expect(status).to.eql({ low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, @@ -78,20 +78,20 @@ describe.only('sympto', () => { it('detects missing temperature shift correctly', function () { const firstExceptionNoShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77, 36.57] - const status = detectTemperatureShift(firstExceptionNoShift) + const status = getTemperatureStatus(firstExceptionNoShift) expect(status).to.eql({ detected: false }) }) it('detects missing temperature shift with not enough high temps', function () { const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] - const status = detectTemperatureShift(temps) + const status = getTemperatureStatus(temps) expect(status).to.eql({ detected: false }) }) it('detects shift after an earlier one was invalid', function () { const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.7, 36.7, 36.7, 36.7] - const status = detectTemperatureShift(temps) + const status = getTemperatureStatus(temps) expect(status).to.eql({ low: [36.4, 36.4, 36.6, 36.6, 36.4, 36.4], ltl: 36.6, @@ -105,7 +105,7 @@ describe.only('sympto', () => { describe('2nd exception rule', () => { it('detects temperature shift with exception temp eql ltl', function () { const secondException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.6, 36.8] - const status = detectTemperatureShift(secondException) + const status = getTemperatureStatus(secondException) expect(status).to.eql({ low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, @@ -117,7 +117,7 @@ describe.only('sympto', () => { it('detects temperature shift with exception temp lower than ltl', function () { const secondException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.8] - const status = detectTemperatureShift(secondException) + const status = getTemperatureStatus(secondException) expect(status).to.eql({ low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, @@ -129,19 +129,19 @@ describe.only('sympto', () => { it('detects missing temperature shift correctly', function () { const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.77, 36.77] - const status = detectTemperatureShift(temps) + const status = getTemperatureStatus(temps) expect(status).to.eql({ detected: false }) }) it('detects missing temperature shift when not enough high temps', function () { const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4] - const status = detectTemperatureShift(temps) + const status = getTemperatureStatus(temps) expect(status).to.eql({ detected: false }) }) it('detects shift after an earlier one was invalid', function () { const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.77, 36.9, 36.9, 36.86, 37.04] - const status = detectTemperatureShift(temps) + const status = getTemperatureStatus(temps) expect(status).to.eql({ low: [36.6, 36.55, 36.8, 36.85, 36.4, 36.75], ltl: 36.85, From 2f8054c1fac7c53b6533bf4a0283d45c01b6f8e8 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 3 Jul 2018 11:14:40 +0200 Subject: [PATCH 17/59] Add mucus shift detection --- lib/sympto/index.js | 5 ++++ lib/sympto/mucus.js | 19 +++++++++++++++ test/sympto/mucus.spec.js | 49 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 lib/sympto/mucus.js create mode 100644 test/sympto/mucus.spec.js diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 92f513d..3fd0102 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -4,4 +4,9 @@ import getMucusStatus from './mucus' export default function (cycleDays) { const temperatureStatus = getTemperatureStatus(cycleDays) const mucusStatus = getMucusStatus(cycleDays) + return { + assumeFertility: true, + temperatureStatus, + mucusStatus + } } \ No newline at end of file diff --git a/lib/sympto/mucus.js b/lib/sympto/mucus.js new file mode 100644 index 0000000..f9c0d82 --- /dev/null +++ b/lib/sympto/mucus.js @@ -0,0 +1,19 @@ +export default function (cycleDays) { + const mucusDays = cycleDays.filter(day => day.mucus && !day.mucus.exclude) + const bestQuality = Math.max(...mucusDays.map(day => day.mucus.value)) + const mucusPeak = mucusDays.find((day, i) => { + if (day.mucus.value !== bestQuality) return false + + const threeFollowingDays = cycleDays.slice(i + 1, i + 4) + if (threeFollowingDays.length < 3) return false + + return threeFollowingDays.every(day => day.mucus.value < bestQuality) + }) + + if (!mucusPeak) return { detected: false } + + return { + detected: true, + mucusPeak + } +} diff --git a/test/sympto/mucus.spec.js b/test/sympto/mucus.spec.js new file mode 100644 index 0000000..c0fd2da --- /dev/null +++ b/test/sympto/mucus.spec.js @@ -0,0 +1,49 @@ +import chai from 'chai' +import getMucusStatus from '../../lib/sympto/mucus' + +const expect = chai.expect + +function turnIntoCycleDayObject(value, fakeDate) { + return { + mucus : { value }, + date: fakeDate + } +} + +describe('sympto', () => { + describe('detect mucus shift', () => { + describe('regular rule', () => { + it('detects mucus shift correctly', function () { + const values = [0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 2, 2, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0] + .map(turnIntoCycleDayObject) + const status = getMucusStatus(values) + expect(status).to.eql({ + detected: true, + mucusPeak: { + date: 10, + mucus: { value: 3 } + } + }) + }) + + it('detects no mucus shift when there are less than 3 days of lower quality', function () { + const values = [0, 1, 1, 2, 0, 0, 1, 2, 3, 2, 3, 3, 3, 2, 2] + .map(turnIntoCycleDayObject) + const status = getMucusStatus(values) + expect(status).to.eql({ detected: false }) + }) + + it('detects no mucus shift when there are no mucus values', function () { + const status = getMucusStatus(Array(10).fill({date: 1, temperature: { value: 35}})) + expect(status).to.eql({ detected: false }) + }) + + it('detects no mucus shift when the mucus values are all the same', function () { + const values = [2, 2, 2, 2, 2, 2, 2, 2] + .map(turnIntoCycleDayObject) + const status = getMucusStatus(values) + expect(status).to.eql({ detected: false }) + }) + }) + }) +}) \ No newline at end of file From 537b8c7647a9437130f8e0112f0f16c6b104920c Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 5 Jul 2018 09:51:54 +0200 Subject: [PATCH 18/59] For 3 following values, look at mucus days, not cycle days --- lib/sympto/mucus.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sympto/mucus.js b/lib/sympto/mucus.js index f9c0d82..0931ad6 100644 --- a/lib/sympto/mucus.js +++ b/lib/sympto/mucus.js @@ -4,7 +4,7 @@ export default function (cycleDays) { const mucusPeak = mucusDays.find((day, i) => { if (day.mucus.value !== bestQuality) return false - const threeFollowingDays = cycleDays.slice(i + 1, i + 4) + const threeFollowingDays = mucusDays.slice(i + 1, i + 4) if (threeFollowingDays.length < 3) return false return threeFollowingDays.every(day => day.mucus.value < bestQuality) From f1f9c7773ab905893fea535f94d3b95acd5464f0 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 5 Jul 2018 12:10:18 +0200 Subject: [PATCH 19/59] Expect cycle days, not numbers, in getTemperatureStatus --- lib/sympto/temperature.js | 160 +++++++++++++++----------------- test/sympto/temperature.spec.js | 106 +++++++++++++++++---- 2 files changed, 164 insertions(+), 102 deletions(-) diff --git a/lib/sympto/temperature.js b/lib/sympto/temperature.js index 86a83be..4779bc0 100644 --- a/lib/sympto/temperature.js +++ b/lib/sympto/temperature.js @@ -1,118 +1,106 @@ -export default function getTemperatureStatus(temperaturesOfCycle) { - // sensiplan rounds temps to the nearest 0.05 - const tempValues = temperaturesOfCycle.map(val => rounded(val, 0.05)) +export default function getTemperatureStatus(cycleDays) { + const temperatureDays = cycleDays + .filter(day => day.temperature && !day.temperature.exclude) + .map(day => { + return { + originalCycleDay: day, + temp: rounded(day.temperature.value, 0.05) + } + }) function getLtl(i) { - const sixTempsBefore = getSixTempsBefore(i) - return Math.max(...sixTempsBefore) - } - function getSixTempsBefore(i) { - return tempValues.slice(0, i).slice(-6) + const daysBefore = temperatureDays.slice(0, i).slice(-6) + const temps = daysBefore.map(day => day.temp) + return Math.max(...temps) } - return tempValues.reduce((acc, temp, i) => { + for (let i = 0; i < temperatureDays.length; i++) { // need at least 6 low temps before we can detect a first high measurement - if (i < 6) return acc - - // if we've already detected a shift, we put it with the other high level temps - if(acc.detected) { - acc.high.push(temp) - return acc - } + if (i < 6) continue // is the temp a candidate for a first high measurement? const ltl = getLtl(i) - if (temp <= ltl) return acc + const temp = temperatureDays[i].temp + if (temp <= ltl) continue - const checkResult = checkIfFirstHighMeasurement(temp, i, tempValues, ltl) - // if we don't have a winner, keep move on to the next candidates - if (!checkResult.isFirstHighMeasurement) return acc + const checkResult = checkIfFirstHighMeasurement(temp, i, temperatureDays, ltl) - // if we do, remember the details and start collecting the high level temps - acc.detected = true - acc.high = [temp] - acc.rule = checkResult.rule - acc.ltl = ltl - acc.low = getSixTempsBefore(i) + if (checkResult.detected) { + checkResult.firstHighMeasurementDay = temperatureDays[i].originalCycleDay + return checkResult + } + } - return acc - }, { - detected: false - }) + return { detected: false } } -function rounded(val, step) { - // we round the difference because of JS decimal weirdness - const inverted = 1 / step - return Math.round(val * inverted) / inverted -} - -function checkIfFirstHighMeasurement(temp, i, temps, ltl) { +function checkIfFirstHighMeasurement(temp, i, temperatureDays, ltl) { // need at least 3 high temps to form a high temperature level - if (i > temps.length - 3) { - return { isFirstHighMeasurement: false } + if (i > temperatureDays.length - 3) { + return { detected: false } } - const nextTemps = temps.slice(i + 1, i + 4) + const nextDays = temperatureDays.slice(i + 1, i + 4) - if (regularRuleApplies(temp, nextTemps, ltl)) { - return { - isFirstHighMeasurement: true, - rule: 0, - ltl - } - } - - if (firstExceptionRuleApplies(temp, nextTemps, ltl)) { - return { - isFirstHighMeasurement: true, - rule: 1, - ltl - } - } - - if (secondExceptionRuleApplies(temp, nextTemps, ltl)) { - return { - isFirstHighMeasurement: true, - rule: 2, - ltl - } - } + return ( + getResultForRegularRule(nextDays, ltl)) || + getResultForFirstExceptionRule(nextDays, ltl) || + getResultForSecondExceptionRule(nextDays, ltl) || + { detected: false } +} +function getResultForRegularRule(nextDays, ltl) { + if (!nextDays.every(day => day.temp > ltl)) return false + const thirdDay = nextDays[1] + if (rounded(thirdDay.temp - ltl, 0.1) < 0.2) return false return { - isFirstHighMeasurement: false + detected: true, + rule: 0, + ltl, + evaluationCompleteDay: thirdDay.originalCycleDay } } -function regularRuleApplies(temp, nextTemps, ltl) { - if (!nextTemps.every(temp => temp > ltl)) return false - const thirdTemp = nextTemps[1] - if (rounded(thirdTemp - ltl, 0.1) < 0.2) return false - return true +function getResultForFirstExceptionRule(nextDays, ltl) { + if (nextDays.length < 3) return false + if (!nextDays.every(day => day.temp > ltl)) return false + const fourthDay = nextDays[2] + if (fourthDay.temp <= ltl) return false + return { + detected: true, + rule: 1, + ltl, + evaluationCompleteDay: fourthDay.originalCycleDay + } } -function firstExceptionRuleApplies(temp, nextTemps, ltl) { - if (nextTemps.length < 3) return false - if (!nextTemps.every(temp => temp > ltl)) return false - const fourthTemp = nextTemps[2] - if (fourthTemp > ltl) return true - return false -} - -function secondExceptionRuleApplies(temp, nextTemps, ltl) { - if (nextTemps.length < 3) return false - if (secondOrThirdTempIsAtOrBelowLtl(nextTemps, ltl)) { - const fourthTemp = nextTemps[2] - if (rounded(fourthTemp - ltl, 0.1) >= 0.2) return true +function getResultForSecondExceptionRule(nextDays, ltl) { + if (nextDays.length < 3) return false + if (secondOrThirdTempIsAtOrBelowLtl(nextDays, ltl)) { + const fourthDay = nextDays[2] + if (rounded(fourthDay.temp - ltl, 0.1) >= 0.2) { + return { + detected: true, + rule: 2, + ltl, + evaluationCompleteDay: fourthDay.originalCycleDay + } + } } return false } -function secondOrThirdTempIsAtOrBelowLtl(nextTemps, ltl) { - const secondIsLow = nextTemps[0] <= ltl - const thirdIsLow = nextTemps[1] <= ltl +function secondOrThirdTempIsAtOrBelowLtl(nextDays, ltl) { + const secondIsLow = nextDays[0].temp <= ltl + const thirdIsLow = nextDays[1].temp <= ltl if ((secondIsLow || thirdIsLow) && !(secondIsLow && thirdIsLow)) { return true } else { return false } -} \ No newline at end of file +} + +function rounded(val, step) { + const inverted = 1 / step + // we round the difference because of JS decimal weirdness + return Math.round(val * inverted) / inverted +} diff --git a/test/sympto/temperature.spec.js b/test/sympto/temperature.spec.js index 8106021..d5131b8 100644 --- a/test/sympto/temperature.spec.js +++ b/test/sympto/temperature.spec.js @@ -3,53 +3,78 @@ import getTemperatureStatus from '../../lib/sympto/temperature' const expect = chai.expect -describe.only('sympto', () => { +function turnIntoCycleDayObject(value, fakeDate) { + return { + temperature : { value }, + date: fakeDate + } +} + +describe('sympto', () => { describe('detect temperature shift', () => { describe('regular rule', () => { it('reports lower temperature status before shift', function () { const lowerTemps = [36.7, 36.57, 36.47, 36.49, 36.57] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(lowerTemps) expect(status).to.eql({ detected: false }) }) it('detects temperature shift correctly', function () { const tempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(tempShift) expect(status).to.eql({ - low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], - ltl: 36.6, - high: [36.8, 36.85, 36.8], detected: true, + ltl: 36.6, + firstHighMeasurementDay: { + date: 7, + temperature: { value: 36.8 } + }, + evaluationCompleteDay: { + date: 9, + temperature: { value: 36.8 } + }, rule: 0 }) }) it('detects no temperature shift when there are no 6 low temps', function () { const tempShift = [36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(tempShift) expect(status).to.eql({ detected: false }) }) it('detects no temperature shift if the shift is not high enough', function () { const tempShift = [36.57, 36.7, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(tempShift) expect(status).to.eql({ detected: false }) }) it('detects missing temperature shift correctly', function () { const noTempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(noTempShift) expect(status).to.eql({ detected: false }) }) it('detects shift after an earlier one was invalid', function () { const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.7, 36.8, 36.9] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) expect(status).to.eql({ - low: [36.4, 36.4, 36.6, 36.6, 36.4, 36.4], ltl: 36.6, - high: [36.7, 36.8, 36.9], + firstHighMeasurementDay: { + date: 10, + temperature: { value: 36.7 } + }, + evaluationCompleteDay: { + date: 12, + temperature: { value: 36.9 } + }, detected: true, rule: 0 }) @@ -57,6 +82,7 @@ describe.only('sympto', () => { it('detects 2 consecutive invalid shifts', function () { const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.6, 36.6, 36.7] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) expect(status).to.eql({ detected: false }) @@ -66,11 +92,19 @@ describe.only('sympto', () => { describe('1st exception rule', () => { it('detects temperature shift', function () { const firstException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77, 36.63] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(firstException) expect(status).to.eql({ - low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, - high: [36.8, 36.85, 36.75, 36.65], + firstHighMeasurementDay: { + date: 7, + temperature: { value: 36.8 } + }, + + evaluationCompleteDay: { + date: 10, + temperature : { value: 36.63 } + }, detected: true, rule: 1 }) @@ -78,38 +112,58 @@ describe.only('sympto', () => { it('detects missing temperature shift correctly', function () { const firstExceptionNoShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77, 36.57] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(firstExceptionNoShift) expect(status).to.eql({ detected: false }) }) it('detects missing temperature shift with not enough high temps', function () { const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) expect(status).to.eql({ detected: false }) + }) it('detects shift after an earlier one was invalid', function () { const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.7, 36.7, 36.7, 36.7] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) expect(status).to.eql({ - low: [36.4, 36.4, 36.6, 36.6, 36.4, 36.4], ltl: 36.6, - high: [36.7, 36.7, 36.7, 36.7], + firstHighMeasurementDay: { + date: 10, + temperature: { value: 36.7 } + }, + + evaluationCompleteDay: { + date: 13, + temperature : { value: 36.7 } + }, detected: true, rule: 1 }) }) + }) describe('2nd exception rule', () => { it('detects temperature shift with exception temp eql ltl', function () { const secondException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.6, 36.8] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(secondException) expect(status).to.eql({ - low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, - high: [36.8, 36.85, 36.6, 36.8], + firstHighMeasurementDay: { + date: 7, + temperature: { value: 36.8 } + }, + + evaluationCompleteDay: { + date: 10, + temperature : { value: 36.8 } + }, detected: true, rule: 2 }) @@ -117,39 +171,59 @@ describe.only('sympto', () => { it('detects temperature shift with exception temp lower than ltl', function () { const secondException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.8] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(secondException) expect(status).to.eql({ - low: [36.55, 36.45, 36.5, 36.55, 36.6, 36.55], ltl: 36.6, - high: [36.8, 36.85, 36.4, 36.8], + firstHighMeasurementDay: { + date: 7, + temperature: { value: 36.8 } + }, + + evaluationCompleteDay: { + date: 10, + temperature : { value: 36.8 } + }, detected: true, rule: 2 }) }) + it('detects missing temperature shift correctly', function () { const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.77, 36.77] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) expect(status).to.eql({ detected: false }) }) it('detects missing temperature shift when not enough high temps', function () { const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) expect(status).to.eql({ detected: false }) }) it('detects shift after an earlier one was invalid', function () { const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.77, 36.9, 36.9, 36.86, 37.04] + .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) expect(status).to.eql({ - low: [36.6, 36.55, 36.8, 36.85, 36.4, 36.75], ltl: 36.85, - high: [36.9, 36.9, 36.85, 37.05], + firstHighMeasurementDay: { + date: 11, + temperature: { value: 36.9 } + }, + + evaluationCompleteDay: { + date: 14, + temperature : { value: 37.04 } + }, detected: true, rule: 2 }) }) + }) }) }) \ No newline at end of file From 836bea778c0cf3e182089aa9caead79796d1da2a Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 5 Jul 2018 13:53:06 +0200 Subject: [PATCH 20/59] Combine temp and mucus evaluation --- lib/sympto/index.js | 21 +++++++---- lib/sympto/mucus.js | 23 +++++++++--- lib/sympto/temperature.js | 2 +- test/sympto/index.spec.js | 77 +++++++++++++++++++++++++++++++++++++++ test/sympto/mucus.spec.js | 6 +-- 5 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 test/sympto/index.spec.js diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 3fd0102..b468f36 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -1,12 +1,19 @@ -import getTemperatureStatus from './temperature' -import getMucusStatus from './mucus' +import getTemperatureShift from './temperature' +import getMucusShift from './mucus' export default function (cycleDays) { - const temperatureStatus = getTemperatureStatus(cycleDays) - const mucusStatus = getMucusStatus(cycleDays) + const assumeFertile = { assumeFertility: true } + + const temperatureShift = getTemperatureShift(cycleDays) + if (!temperatureShift.detected) return assumeFertile + + const tempEvalEndIndex = cycleDays.indexOf(temperatureShift.evaluationCompleteDay) + const mucusShift = getMucusShift(cycleDays, tempEvalEndIndex) + if (!mucusShift.detected) return assumeFertile + return { - assumeFertility: true, - temperatureStatus, - mucusStatus + assumeFertility: false, + temperatureShift, + mucusShift } } \ No newline at end of file diff --git a/lib/sympto/mucus.js b/lib/sympto/mucus.js index 0931ad6..ada8127 100644 --- a/lib/sympto/mucus.js +++ b/lib/sympto/mucus.js @@ -1,13 +1,25 @@ -export default function (cycleDays) { +export default function (cycleDays, tempEvalEndIndex) { const mucusDays = cycleDays.filter(day => day.mucus && !day.mucus.exclude) const bestQuality = Math.max(...mucusDays.map(day => day.mucus.value)) - const mucusPeak = mucusDays.find((day, i) => { - if (day.mucus.value !== bestQuality) return false - const threeFollowingDays = mucusDays.slice(i + 1, i + 4) + const mucusPeak = cycleDays.find((day, i) => { + if (!mucusDays.includes(day) || day.mucus.value !== bestQuality) return false + + // sensiplan says the three following days must be of lower quality + // AND no best quality day may occur until temperature evaluation has + // been completed + const mucusDaysIndex = mucusDays.indexOf(day) + const threeFollowingDays = mucusDays.slice(mucusDaysIndex + 1, mucusDaysIndex + 4) if (threeFollowingDays.length < 3) return false - return threeFollowingDays.every(day => day.mucus.value < bestQuality) + const bestQualityOccurringIn3FollowingDays = threeFollowingDays.some(day => day.mucus.value >= bestQuality) + if (bestQualityOccurringIn3FollowingDays) return false + + const relevantDays = cycleDays + .slice(i + 1, tempEvalEndIndex + 1) + .filter(day => day.mucus && !day.mucus.exclude) + + return relevantDays.every(day => day.mucus.value < bestQuality) }) if (!mucusPeak) return { detected: false } @@ -17,3 +29,4 @@ export default function (cycleDays) { mucusPeak } } + diff --git a/lib/sympto/temperature.js b/lib/sympto/temperature.js index 4779bc0..af90282 100644 --- a/lib/sympto/temperature.js +++ b/lib/sympto/temperature.js @@ -1,4 +1,4 @@ -export default function getTemperatureStatus(cycleDays) { +export default function (cycleDays) { const temperatureDays = cycleDays .filter(day => day.temperature && !day.temperature.exclude) .map(day => { diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js new file mode 100644 index 0000000..bbea1ae --- /dev/null +++ b/test/sympto/index.spec.js @@ -0,0 +1,77 @@ +import chai from 'chai' +import getSensiplanStatus from '../../lib/sympto' + +const expect = chai.expect + +function convertToSymptoFormat(val, i) { + return { + date: i, + temperature: val.temperature ? { value: val.temperature } : null, + mucus: val.mucus ? { value: val.mucus } : null + } +} + +describe('sympto', () => { + describe('evaluating mucus and temperature shift together', () => { + it('reports fertile when mucus reaches best quality again within temperature evaluation phase', function () { + const values = [ + { temperature: 36.6 }, + { temperature: 36.65 }, + { temperature: 36.5 }, + { temperature: 36.6 }, + { temperature: 36.55 }, + { temperature: 36.7, mucus: 0 }, + { temperature: 36.75, mucus: 0 }, + { temperature: 36.45, mucus: 1 }, + { temperature: 36.5, mucus: 4 }, + { temperature: 36.4, mucus: 2 }, + { temperature: 36.5, mucus: 3 }, + { temperature: 36.55, mucus: 3 }, + { temperature: 36.45, mucus: 3 }, + { temperature: 36.5, mucus: 4 }, + { temperature: 36.55, mucus: 4 }, + { temperature: 36.7, mucus: 3 }, + { temperature: 36.65, mucus: 3 }, + { temperature: 36.75, mucus: 4 }, + { temperature: 36.8, mucus: 1 }, + { temperature: 36.85, mucus: 2 }, + { temperature: 36.8, mucus: 2 }, + { temperature: 36.9, mucus: 2 }, + { temperature: 36.9, mucus: 1 }, + { temperature: 36.85, mucus: 1 }, + { temperature: 36.9, mucus: 1 }, + { temperature: 36.8, mucus: 1 }, + { temperature: 36.9, mucus: 1 } + ] + + const temperatures = values.map(convertToSymptoFormat) + const status = getSensiplanStatus(temperatures) + expect(status).to.eql({ + assumeFertility: false, + temperatureShift: { + detected: true, + ltl: 36.55, + rule: 0, + firstHighMeasurementDay: { + date: 15, + temperature: { value: 36.7 }, + mucus: { value: 3 } + }, + evaluationCompleteDay: { + date: 17, + temperature: { value: 36.75 }, + mucus: { value: 4 } + } + }, + mucusShift: { + detected: true, + mucusPeak: { + date: 17, + mucus: { value: 4 }, + temperature: { value: 36.75 } + } + } + }) + }) + }) +}) \ No newline at end of file diff --git a/test/sympto/mucus.spec.js b/test/sympto/mucus.spec.js index c0fd2da..7e9b704 100644 --- a/test/sympto/mucus.spec.js +++ b/test/sympto/mucus.spec.js @@ -16,7 +16,7 @@ describe('sympto', () => { it('detects mucus shift correctly', function () { const values = [0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 2, 2, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0] .map(turnIntoCycleDayObject) - const status = getMucusStatus(values) + const status = getMucusStatus(values, 30) expect(status).to.eql({ detected: true, mucusPeak: { @@ -29,7 +29,7 @@ describe('sympto', () => { it('detects no mucus shift when there are less than 3 days of lower quality', function () { const values = [0, 1, 1, 2, 0, 0, 1, 2, 3, 2, 3, 3, 3, 2, 2] .map(turnIntoCycleDayObject) - const status = getMucusStatus(values) + const status = getMucusStatus(values, 30) expect(status).to.eql({ detected: false }) }) @@ -41,7 +41,7 @@ describe('sympto', () => { it('detects no mucus shift when the mucus values are all the same', function () { const values = [2, 2, 2, 2, 2, 2, 2, 2] .map(turnIntoCycleDayObject) - const status = getMucusStatus(values) + const status = getMucusStatus(values, 30) expect(status).to.eql({ detected: false }) }) }) From 9102bce7ce059483f5ba30790f1f8e75e089ad90 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 5 Jul 2018 14:28:21 +0200 Subject: [PATCH 21/59] Return cycle phase starts --- lib/sympto/index.js | 22 ++++++++++++++++++---- test/sympto/index.spec.js | 21 +++++++++++++-------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index b468f36..a4952bd 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -2,18 +2,32 @@ import getTemperatureShift from './temperature' import getMucusShift from './mucus' export default function (cycleDays) { - const assumeFertile = { assumeFertility: true } + const assumeFertileStatus = { assumeFertility: true } + // TODO second phase calculation + if (cycleDays.length) assumeFertileStatus.phases = [ + { + startDate: cycleDays[0].date, + startTime: '00:00' + }, + 'TODO' + ] const temperatureShift = getTemperatureShift(cycleDays) - if (!temperatureShift.detected) return assumeFertile + if (!temperatureShift.detected) return assumeFertileStatus const tempEvalEndIndex = cycleDays.indexOf(temperatureShift.evaluationCompleteDay) const mucusShift = getMucusShift(cycleDays, tempEvalEndIndex) - if (!mucusShift.detected) return assumeFertile + if (!mucusShift.detected) return assumeFertileStatus + + const phase2 = { + startDate: temperatureShift.evaluationCompleteDay.date, + startTime: '18:00' + } return { assumeFertility: false, temperatureShift, - mucusShift + mucusShift, + phases: assumeFertileStatus.phases.concat(phase2) } } \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index bbea1ae..65550a0 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -4,18 +4,18 @@ import getSensiplanStatus from '../../lib/sympto' const expect = chai.expect function convertToSymptoFormat(val, i) { - return { - date: i, - temperature: val.temperature ? { value: val.temperature } : null, - mucus: val.mucus ? { value: val.mucus } : null - } + const sympto = { date: i } + if (val.temperature) sympto.temperature = { value: val.temperature } + if (val.mucus) sympto.mucus = { value: val.mucus } + if (val.bleeding) sympto.bleeding = { value: val.bleeding } + return sympto } describe('sympto', () => { describe('evaluating mucus and temperature shift together', () => { it('reports fertile when mucus reaches best quality again within temperature evaluation phase', function () { const values = [ - { temperature: 36.6 }, + { temperature: 36.6 , bleeding: 2 }, { temperature: 36.65 }, { temperature: 36.5 }, { temperature: 36.6 }, @@ -48,6 +48,11 @@ describe('sympto', () => { const status = getSensiplanStatus(temperatures) expect(status).to.eql({ assumeFertility: false, + phases: [ + { startDate: 0, startTime: '00:00'}, + 'TODO', + { startDate: 17, startTime: '18:00'} + ], temperatureShift: { detected: true, ltl: 36.55, @@ -60,7 +65,7 @@ describe('sympto', () => { evaluationCompleteDay: { date: 17, temperature: { value: 36.75 }, - mucus: { value: 4 } + mucus: { value: 4 }, } }, mucusShift: { @@ -68,7 +73,7 @@ describe('sympto', () => { mucusPeak: { date: 17, mucus: { value: 4 }, - temperature: { value: 36.75 } + temperature: { value: 36.75 }, } } }) From 77ad491d300551ae4d5ee133f4dd84679bd2df7e Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 10 Jul 2018 10:55:33 +0200 Subject: [PATCH 22/59] Return evaluation completed day from mucus eval --- lib/sympto/mucus.js | 34 ++++++++++++++++++++-------------- test/sympto/mucus.spec.js | 6 +++++- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/sympto/mucus.js b/lib/sympto/mucus.js index ada8127..f848881 100644 --- a/lib/sympto/mucus.js +++ b/lib/sympto/mucus.js @@ -2,31 +2,37 @@ export default function (cycleDays, tempEvalEndIndex) { const mucusDays = cycleDays.filter(day => day.mucus && !day.mucus.exclude) const bestQuality = Math.max(...mucusDays.map(day => day.mucus.value)) - const mucusPeak = cycleDays.find((day, i) => { - if (!mucusDays.includes(day) || day.mucus.value !== bestQuality) return false + for (let i = 0; i < mucusDays.length; i++) { + const day = mucusDays[i] + if (day.mucus.value !== bestQuality) continue // sensiplan says the three following days must be of lower quality // AND no best quality day may occur until temperature evaluation has // been completed - const mucusDaysIndex = mucusDays.indexOf(day) - const threeFollowingDays = mucusDays.slice(mucusDaysIndex + 1, mucusDaysIndex + 4) - if (threeFollowingDays.length < 3) return false + const threeFollowingDays = mucusDays.slice(i + 1, i + 4) + if (threeFollowingDays.length < 3) continue const bestQualityOccurringIn3FollowingDays = threeFollowingDays.some(day => day.mucus.value >= bestQuality) - if (bestQualityOccurringIn3FollowingDays) return false + if (bestQualityOccurringIn3FollowingDays) continue + // FIXME mucus peak can be same day as first higher measurement + const cycleDayIndex = cycleDays.indexOf(day) const relevantDays = cycleDays - .slice(i + 1, tempEvalEndIndex + 1) + .slice(cycleDayIndex + 1, tempEvalEndIndex + 1) .filter(day => day.mucus && !day.mucus.exclude) - return relevantDays.every(day => day.mucus.value < bestQuality) - }) + const evaluationCompleteDay = relevantDays.length > 3 ? + relevantDays[relevantDays.length - 1] : threeFollowingDays[threeFollowingDays.length - 1] - if (!mucusPeak) return { detected: false } - - return { - detected: true, - mucusPeak + if (relevantDays.every(day => day.mucus.value < bestQuality)) { + return { + detected: true, + mucusPeak: day, + evaluationCompleteDay + } + } } + + return { detected: false } } diff --git a/test/sympto/mucus.spec.js b/test/sympto/mucus.spec.js index 7e9b704..f8b83d2 100644 --- a/test/sympto/mucus.spec.js +++ b/test/sympto/mucus.spec.js @@ -16,12 +16,16 @@ describe('sympto', () => { it('detects mucus shift correctly', function () { const values = [0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 2, 2, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0] .map(turnIntoCycleDayObject) - const status = getMucusStatus(values, 30) + const status = getMucusStatus(values, 12) expect(status).to.eql({ detected: true, mucusPeak: { date: 10, mucus: { value: 3 } + }, + evaluationCompleteDay: { + date: 13, + mucus: { value: 0 } } }) }) From 0179c4588ed957c608cf9890ca7e8bb99638baf8 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 10 Jul 2018 10:56:33 +0200 Subject: [PATCH 23/59] Detect pre- and postovulatory phase with no previous higher measurement --- lib/sympto/index.js | 77 ++++++++++++++------- test/sympto/index.spec.js | 142 ++++++++++++++++++++++++++++---------- 2 files changed, 157 insertions(+), 62 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index a4952bd..8e448fe 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -1,33 +1,58 @@ import getTemperatureShift from './temperature' import getMucusShift from './mucus' -export default function (cycleDays) { - const assumeFertileStatus = { assumeFertility: true } - // TODO second phase calculation - if (cycleDays.length) assumeFertileStatus.phases = [ - { - startDate: cycleDays[0].date, - startTime: '00:00' +export default function ({ cycle, previousCycle }) { + // TODO check for basic stuff, throw if nonexistent + const status = { + assumeFertility: true, + phases: { + periOvulatory: { + start: { + date: null, + time: '00:00' + }, + cycleDays: null + } + } + } + + // if there was no first higher measurement in the previous cycle, + // no infertile preovulatory phase may be assumed + + if (getTemperatureShift(previousCycle).detected) { + // add preOvulatory phase + } else { + const first = cycle[0] + status.phases.periOvulatory.start.date = first.date + status.phases.periOvulatory.cycleDays = [...cycle] + } + + const temperatureShift = getTemperatureShift(cycle) + if (!temperatureShift.detected) return status + + const tempEvalEndIndex = cycle.indexOf(temperatureShift.evaluationCompleteDay) + const mucusShift = getMucusShift(cycle, tempEvalEndIndex) + if (!mucusShift.detected) return status + + const periOvulatoryEnd = + temperatureShift.evaluationCompleteDay.date > mucusShift.evaluationCompleteDay.date ? + temperatureShift.evaluationCompleteDay : mucusShift.evaluationCompleteDay + + const prevPeriOvulatoryDays = status.phases.periOvulatory.cycleDays + const periOvulatoryEndIndex = prevPeriOvulatoryDays.indexOf(periOvulatoryEnd) + + status.phases.postOvulatory = { + start: { + date: periOvulatoryEnd.date, + time: '18:00' }, - 'TODO' - ] - - const temperatureShift = getTemperatureShift(cycleDays) - if (!temperatureShift.detected) return assumeFertileStatus - - const tempEvalEndIndex = cycleDays.indexOf(temperatureShift.evaluationCompleteDay) - const mucusShift = getMucusShift(cycleDays, tempEvalEndIndex) - if (!mucusShift.detected) return assumeFertileStatus - - const phase2 = { - startDate: temperatureShift.evaluationCompleteDay.date, - startTime: '18:00' + cycleDays: prevPeriOvulatoryDays.slice(periOvulatoryEndIndex) } - return { - assumeFertility: false, - temperatureShift, - mucusShift, - phases: assumeFertileStatus.phases.concat(phase2) - } + status.phases.periOvulatory.cycleDays = prevPeriOvulatoryDays.slice(0, periOvulatoryEndIndex + 1) + + status.mucusShift = mucusShift + status.temperatureShift = temperatureShift + + return status } \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index 65550a0..b27ca98 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -11,48 +11,117 @@ function convertToSymptoFormat(val, i) { return sympto } +const cycleWithTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8, 36.8] + .map(num => ({ temperature: num })) + .map(convertToSymptoFormat) + +const cycleWithoutTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8] + .map(num => ({ temperature: num })) + .map(convertToSymptoFormat) + +const cycleWithTempAndMucusShift = [ + { temperature: 36.6, bleeding: 2 }, + { temperature: 36.65 }, + { temperature: 36.5 }, + { temperature: 36.6 }, + { temperature: 36.55 }, + { temperature: 36.7, mucus: 0 }, + { temperature: 36.75, mucus: 0 }, + { temperature: 36.45, mucus: 1 }, + { temperature: 36.5, mucus: 4 }, + { temperature: 36.4, mucus: 2 }, + { temperature: 36.5, mucus: 3 }, + { temperature: 36.55, mucus: 3 }, + { temperature: 36.45, mucus: 3 }, + { temperature: 36.5, mucus: 4 }, + { temperature: 36.55, mucus: 4 }, + { temperature: 36.7, mucus: 3 }, + { temperature: 36.65, mucus: 3 }, + { temperature: 36.75, mucus: 4 }, + { temperature: 36.8, mucus: 1 }, + { temperature: 36.85, mucus: 2 }, + { temperature: 36.8, mucus: 2 }, + { temperature: 36.9, mucus: 2 }, + { temperature: 36.9, mucus: 1 }, + { temperature: 36.85, mucus: 1 }, + { temperature: 36.9, mucus: 1 }, + { temperature: 36.8, mucus: 1 }, + { temperature: 36.9, mucus: 1 } +].map(convertToSymptoFormat) + describe('sympto', () => { describe('evaluating mucus and temperature shift together', () => { - it('reports fertile when mucus reaches best quality again within temperature evaluation phase', function () { - const values = [ - { temperature: 36.6 , bleeding: 2 }, - { temperature: 36.65 }, - { temperature: 36.5 }, - { temperature: 36.6 }, - { temperature: 36.55 }, - { temperature: 36.7, mucus: 0 }, - { temperature: 36.75, mucus: 0 }, - { temperature: 36.45, mucus: 1 }, - { temperature: 36.5, mucus: 4 }, - { temperature: 36.4, mucus: 2 }, - { temperature: 36.5, mucus: 3 }, - { temperature: 36.55, mucus: 3 }, - { temperature: 36.45, mucus: 3 }, - { temperature: 36.5, mucus: 4 }, - { temperature: 36.55, mucus: 4 }, - { temperature: 36.7, mucus: 3 }, - { temperature: 36.65, mucus: 3 }, - { temperature: 36.75, mucus: 4 }, - { temperature: 36.8, mucus: 1 }, - { temperature: 36.85, mucus: 2 }, - { temperature: 36.8, mucus: 2 }, - { temperature: 36.9, mucus: 2 }, - { temperature: 36.9, mucus: 1 }, - { temperature: 36.85, mucus: 1 }, - { temperature: 36.9, mucus: 1 }, - { temperature: 36.8, mucus: 1 }, - { temperature: 36.9, mucus: 1 } - ] + describe('with no previous higher measurement', () => { + it('with no shifts detects only periovulatory', function () { + const values = [ + { temperature: 36.6, bleeding: 2 }, + { temperature: 36.65 }, + { temperature: 36.5 }, + { temperature: 36.6 }, + { temperature: 36.55 }, + { temperature: 36.7, mucus: 0 }, + { temperature: 36.75, mucus: 0 }, + { temperature: 36.45, mucus: 1 } + ] - const temperatures = values.map(convertToSymptoFormat) + const cycle = values.map(convertToSymptoFormat) + const status = getSensiplanStatus({ + cycle, + previousCycle: cycleWithoutTempShift + }) + + expect(status).to.eql({ + assumeFertility: true, + phases: { + periOvulatory: { + start: { + date: 0, + time: '00:00' + }, + cycleDays: cycle + } + }, + }) + }) + + it('with shifts detects only periovulatory and postovulatory', function () { + const status = getSensiplanStatus({ + cycle: cycleWithTempAndMucusShift, + previousCycle: cycleWithoutTempShift + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + expect(status.assumeFertility).to.be.true() + expect(Object.keys(status.phases)).to.eql(['periOvulatory', 'postOvulatory']) + expect(status.phases.periOvulatory).to.eql({ + start: { + date: 0, + time: '00:00' + }, + cycleDays: cycleWithTempAndMucusShift.slice(0, 21) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: 20, + time: '18:00' + }, + cycleDays: cycleWithTempAndMucusShift.slice(20) + }) + }) + }) + }) + describe('with shifts', () => { + it.skip('reports fertile when mucus reaches best quality again within temperature evaluation phase', function () { const status = getSensiplanStatus(temperatures) expect(status).to.eql({ assumeFertility: false, - phases: [ - { startDate: 0, startTime: '00:00'}, - 'TODO', - { startDate: 17, startTime: '18:00'} - ], + phases: { + preOvulatory: + { startDate: 0, startTime: '00:00' }, + periOvulatory: 'TODO', + postOvulatory: { startDate: 17, startTime: '18:00' } + }, temperatureShift: { detected: true, ltl: 36.55, @@ -78,5 +147,6 @@ describe('sympto', () => { } }) }) + }) }) \ No newline at end of file From f7e1152fdc9f5cfd7a8fb1edb3c8955651a50157 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 10 Jul 2018 12:35:40 +0200 Subject: [PATCH 24/59] Implement 5-day-rule --- lib/sympto/index.js | 21 +++++--- lib/sympto/pre-ovulatory.js | 21 ++++++++ test/sympto/index.spec.js | 98 ++++++++++++++++++++----------------- 3 files changed, 87 insertions(+), 53 deletions(-) create mode 100644 lib/sympto/pre-ovulatory.js diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 8e448fe..4469ec3 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -1,5 +1,7 @@ import getTemperatureShift from './temperature' import getMucusShift from './mucus' +import getPreOvulatoryPhase from './pre-ovulatory' +import { LocalDate } from 'js-joda' export default function ({ cycle, previousCycle }) { // TODO check for basic stuff, throw if nonexistent @@ -8,8 +10,7 @@ export default function ({ cycle, previousCycle }) { phases: { periOvulatory: { start: { - date: null, - time: '00:00' + date: null }, cycleDays: null } @@ -18,13 +19,19 @@ export default function ({ cycle, previousCycle }) { // if there was no first higher measurement in the previous cycle, // no infertile preovulatory phase may be assumed + if (getTemperatureShift(previousCycle).detected && !cycle[0].mucus) { + status.phases.preOvulatory = getPreOvulatoryPhase(cycle) + } - if (getTemperatureShift(previousCycle).detected) { - // add preOvulatory phase + const periPhase = status.phases.periOvulatory + if (status.phases.preOvulatory) { + const prePhase = status.phases.preOvulatory + periPhase.start.date = LocalDate.parse(prePhase.end.date).plusDays(1).toString() + const lastPreDay = prePhase.cycleDays[prePhase.cycleDays.length - 1] + periPhase.cycleDays = cycle.slice(cycle.indexOf(lastPreDay) + 1) } else { - const first = cycle[0] - status.phases.periOvulatory.start.date = first.date - status.phases.periOvulatory.cycleDays = [...cycle] + periPhase.start.date = cycle[0].date + periPhase.cycleDays = [...cycle] } const temperatureShift = getTemperatureShift(cycle) diff --git a/lib/sympto/pre-ovulatory.js b/lib/sympto/pre-ovulatory.js new file mode 100644 index 0000000..634b410 --- /dev/null +++ b/lib/sympto/pre-ovulatory.js @@ -0,0 +1,21 @@ +export default function(cycle) { + const fiveDayRuleDays = cycle.slice(0, 5) + const preOvulatoryDays = getDaysUntilFertileMucus(fiveDayRuleDays) + return { + cycleDays: preOvulatoryDays, + start: { + date: preOvulatoryDays[0].date + }, + end: { + date: preOvulatoryDays[preOvulatoryDays.length - 1].date, + } + } +} + +function getDaysUntilFertileMucus(days) { + const firstFertileMucusDayIndex = days.findIndex(day => day.mucus && day.mucus.value > 1) + if (firstFertileMucusDayIndex > -1) { + return days.slice(0, firstFertileMucusDayIndex) + } + return days +} \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index b27ca98..3bfb46f 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -4,7 +4,9 @@ import getSensiplanStatus from '../../lib/sympto' const expect = chai.expect function convertToSymptoFormat(val, i) { - const sympto = { date: i } + ++i + const dayString = i < 10 ? `0${i}` : i + const sympto = { date: `2018-06-${dayString}` } if (val.temperature) sympto.temperature = { value: val.temperature } if (val.mucus) sympto.mucus = { value: val.mucus } if (val.bleeding) sympto.bleeding = { value: val.bleeding } @@ -49,6 +51,31 @@ const cycleWithTempAndMucusShift = [ { temperature: 36.9, mucus: 1 } ].map(convertToSymptoFormat) +const cycleWithTempAndNoMucusShift = [ + { temperature: 36.6, bleeding: 2 }, + { temperature: 36.65 }, + { temperature: 36.5 }, + { temperature: 36.6 }, + { temperature: 36.55 }, + { temperature: 36.7, mucus: 0 }, + { temperature: 36.75, mucus: 0 }, + { temperature: 36.45, mucus: 1 }, + { temperature: 36.5, mucus: 4 }, + { temperature: 36.4, mucus: 2 }, + { temperature: 36.5, mucus: 3 }, + { temperature: 36.55, mucus: 3 }, + { temperature: 36.45, mucus: 3 }, + { temperature: 36.5, mucus: 4 }, + { temperature: 36.55, mucus: 4 }, + { temperature: 36.7, mucus: 3 }, + { temperature: 36.65, mucus: 3 }, + { temperature: 36.75, mucus: 4 }, + { temperature: 36.8, mucus: 4 }, + { temperature: 36.85, mucus: 4 }, + { temperature: 36.8, mucus: 4 }, + { temperature: 36.9, mucus: 4 } +].map(convertToSymptoFormat) + describe('sympto', () => { describe('evaluating mucus and temperature shift together', () => { describe('with no previous higher measurement', () => { @@ -74,10 +101,7 @@ describe('sympto', () => { assumeFertility: true, phases: { periOvulatory: { - start: { - date: 0, - time: '00:00' - }, + start: { date: '2018-06-01' }, cycleDays: cycle } }, @@ -95,15 +119,12 @@ describe('sympto', () => { expect(status.assumeFertility).to.be.true() expect(Object.keys(status.phases)).to.eql(['periOvulatory', 'postOvulatory']) expect(status.phases.periOvulatory).to.eql({ - start: { - date: 0, - time: '00:00' - }, + start: { date: '2018-06-01' }, cycleDays: cycleWithTempAndMucusShift.slice(0, 21) }) expect(status.phases.postOvulatory).to.eql({ start: { - date: 20, + date: '2018-06-21', time: '18:00' }, cycleDays: cycleWithTempAndMucusShift.slice(20) @@ -111,42 +132,27 @@ describe('sympto', () => { }) }) }) - describe('with shifts', () => { - it.skip('reports fertile when mucus reaches best quality again within temperature evaluation phase', function () { - const status = getSensiplanStatus(temperatures) - expect(status).to.eql({ - assumeFertility: false, - phases: { - preOvulatory: - { startDate: 0, startTime: '00:00' }, - periOvulatory: 'TODO', - postOvulatory: { startDate: 17, startTime: '18:00' } - }, - temperatureShift: { - detected: true, - ltl: 36.55, - rule: 0, - firstHighMeasurementDay: { - date: 15, - temperature: { value: 36.7 }, - mucus: { value: 3 } - }, - evaluationCompleteDay: { - date: 17, - temperature: { value: 36.75 }, - mucus: { value: 4 }, - } - }, - mucusShift: { - detected: true, - mucusPeak: { - date: 17, - mucus: { value: 4 }, - temperature: { value: 36.75 }, - } - } - }) - }) + describe('with previous higher measurement', () => { + describe('with no shifts detects pre- and periovulatory phase', function () { + it('according to 5-day-rule', function () { + const status = getSensiplanStatus({ + cycle: cycleWithTempAndNoMucusShift, + previousCycle: cycleWithTempShift + }) + expect(Object.keys(status.phases)).to.eql(['periOvulatory', 'preOvulatory']) + expect(status.assumeFertility).to.be.true() + expect(status.phases.preOvulatory).to.eql({ + cycleDays: cycleWithTempAndNoMucusShift.slice(0,5), + start: { date: '2018-06-01' }, + end: { date: '2018-06-05' } + }) + expect(status.phases.periOvulatory).to.eql({ + cycleDays: cycleWithTempAndNoMucusShift.slice(5), + start: { date: '2018-06-06' } + }) + }) + + }) }) }) \ No newline at end of file From 672235a475a601a3cc2d48b79ef292d14f082993 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 10 Jul 2018 13:03:19 +0200 Subject: [PATCH 25/59] Detect all 3 phases --- lib/sympto/index.js | 8 +++++--- test/sympto/index.spec.js | 39 +++++++++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 4469ec3..2d2f244 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -18,7 +18,7 @@ export default function ({ cycle, previousCycle }) { } // if there was no first higher measurement in the previous cycle, - // no infertile preovulatory phase may be assumed + // no infertile pre-ovulatory phase may be assumed if (getTemperatureShift(previousCycle).detected && !cycle[0].mucus) { status.phases.preOvulatory = getPreOvulatoryPhase(cycle) } @@ -45,7 +45,7 @@ export default function ({ cycle, previousCycle }) { temperatureShift.evaluationCompleteDay.date > mucusShift.evaluationCompleteDay.date ? temperatureShift.evaluationCompleteDay : mucusShift.evaluationCompleteDay - const prevPeriOvulatoryDays = status.phases.periOvulatory.cycleDays + const prevPeriOvulatoryDays = periPhase.cycleDays const periOvulatoryEndIndex = prevPeriOvulatoryDays.indexOf(periOvulatoryEnd) status.phases.postOvulatory = { @@ -56,10 +56,12 @@ export default function ({ cycle, previousCycle }) { cycleDays: prevPeriOvulatoryDays.slice(periOvulatoryEndIndex) } - status.phases.periOvulatory.cycleDays = prevPeriOvulatoryDays.slice(0, periOvulatoryEndIndex + 1) + periPhase.cycleDays = prevPeriOvulatoryDays.slice(0, periOvulatoryEndIndex + 1) + periPhase.end = status.phases.postOvulatory.start status.mucusShift = mucusShift status.temperatureShift = temperatureShift + status.assumeFertility = false return status } \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index 3bfb46f..0f02976 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -79,7 +79,7 @@ const cycleWithTempAndNoMucusShift = [ describe('sympto', () => { describe('evaluating mucus and temperature shift together', () => { describe('with no previous higher measurement', () => { - it('with no shifts detects only periovulatory', function () { + it('with no shifts detects only peri-ovulatory', function () { const values = [ { temperature: 36.6, bleeding: 2 }, { temperature: 36.65 }, @@ -108,7 +108,7 @@ describe('sympto', () => { }) }) - it('with shifts detects only periovulatory and postovulatory', function () { + it('with shifts detects only peri-ovulatory and post-ovulatory', function () { const status = getSensiplanStatus({ cycle: cycleWithTempAndMucusShift, previousCycle: cycleWithoutTempShift @@ -116,10 +116,11 @@ describe('sympto', () => { expect(status.temperatureShift).to.be.an('object') expect(status.mucusShift).to.be.an('object') - expect(status.assumeFertility).to.be.true() - expect(Object.keys(status.phases)).to.eql(['periOvulatory', 'postOvulatory']) + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(2) expect(status.phases.periOvulatory).to.eql({ start: { date: '2018-06-01' }, + end: { date: '2018-06-21', time: '18:00' }, cycleDays: cycleWithTempAndMucusShift.slice(0, 21) }) expect(status.phases.postOvulatory).to.eql({ @@ -133,14 +134,14 @@ describe('sympto', () => { }) }) describe('with previous higher measurement', () => { - describe('with no shifts detects pre- and periovulatory phase', function () { + describe('with no shifts detects pre- and peri-ovulatory phase', function () { it('according to 5-day-rule', function () { const status = getSensiplanStatus({ cycle: cycleWithTempAndNoMucusShift, previousCycle: cycleWithTempShift }) - expect(Object.keys(status.phases)).to.eql(['periOvulatory', 'preOvulatory']) + expect(Object.keys(status.phases).length).to.eql(2) expect(status.assumeFertility).to.be.true() expect(status.phases.preOvulatory).to.eql({ cycleDays: cycleWithTempAndNoMucusShift.slice(0,5), @@ -154,5 +155,31 @@ describe('sympto', () => { }) }) + describe('with shifts detects pre- and peri-ovulatory phase', function () { + it('according to 5-day-rule', function () { + const status = getSensiplanStatus({ + cycle: cycleWithTempAndMucusShift, + previousCycle: cycleWithTempShift + }) + + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.assumeFertility).to.be.false() + expect(status.phases.preOvulatory).to.eql({ + cycleDays: cycleWithTempAndMucusShift.slice(0,5), + start: { date: '2018-06-01' }, + end: { date: '2018-06-05' } + }) + expect(status.phases.periOvulatory).to.eql({ + cycleDays: cycleWithTempAndMucusShift.slice(5, 21), + start: { date: '2018-06-06' }, + end: { date: '2018-06-21', time: '18:00'} + }) + expect(status.phases.postOvulatory).to.eql({ + cycleDays: cycleWithTempAndMucusShift.slice(20), + start: { date: '2018-06-21', time: '18:00'} + }) + }) + + }) }) }) \ No newline at end of file From 51bd75fb0f5c4d15a1344e78d6690a3ab5817df5 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 10 Jul 2018 13:10:53 +0200 Subject: [PATCH 26/59] Extract fixtures --- test/sympto/fixtures.js | 92 +++++++++++++++++++++++++++++++++++++++ test/sympto/index.spec.js | 92 +++------------------------------------ 2 files changed, 97 insertions(+), 87 deletions(-) create mode 100644 test/sympto/fixtures.js diff --git a/test/sympto/fixtures.js b/test/sympto/fixtures.js new file mode 100644 index 0000000..14d1eb2 --- /dev/null +++ b/test/sympto/fixtures.js @@ -0,0 +1,92 @@ + +function convertToSymptoFormat(val, i) { + ++i + const dayString = i < 10 ? `0${i}` : i + const sympto = { date: `2018-06-${dayString}` } + if (val.temperature) sympto.temperature = { value: val.temperature } + if (val.mucus) sympto.mucus = { value: val.mucus } + if (val.bleeding) sympto.bleeding = { value: val.bleeding } + return sympto +} + +const cycleWithTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8, 36.8] + .map(num => ({ temperature: num })) + .map(convertToSymptoFormat) + +const cycleWithoutTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8] + .map(num => ({ temperature: num })) + .map(convertToSymptoFormat) + +const cycleWithTempAndMucusShift = [ + { temperature: 36.6, bleeding: 2 }, + { temperature: 36.65 }, + { temperature: 36.5 }, + { temperature: 36.6 }, + { temperature: 36.55 }, + { temperature: 36.7, mucus: 0 }, + { temperature: 36.75, mucus: 0 }, + { temperature: 36.45, mucus: 1 }, + { temperature: 36.5, mucus: 4 }, + { temperature: 36.4, mucus: 2 }, + { temperature: 36.5, mucus: 3 }, + { temperature: 36.55, mucus: 3 }, + { temperature: 36.45, mucus: 3 }, + { temperature: 36.5, mucus: 4 }, + { temperature: 36.55, mucus: 4 }, + { temperature: 36.7, mucus: 3 }, + { temperature: 36.65, mucus: 3 }, + { temperature: 36.75, mucus: 4 }, + { temperature: 36.8, mucus: 1 }, + { temperature: 36.85, mucus: 2 }, + { temperature: 36.8, mucus: 2 }, + { temperature: 36.9, mucus: 2 }, + { temperature: 36.9, mucus: 1 }, + { temperature: 36.85, mucus: 1 }, + { temperature: 36.9, mucus: 1 }, + { temperature: 36.8, mucus: 1 }, + { temperature: 36.9, mucus: 1 } +].map(convertToSymptoFormat) + +const cycleWithTempAndNoMucusShift = [ + { temperature: 36.6, bleeding: 2 }, + { temperature: 36.65 }, + { temperature: 36.5 }, + { temperature: 36.6 }, + { temperature: 36.55 }, + { temperature: 36.7, mucus: 0 }, + { temperature: 36.75, mucus: 0 }, + { temperature: 36.45, mucus: 1 }, + { temperature: 36.5, mucus: 4 }, + { temperature: 36.4, mucus: 2 }, + { temperature: 36.5, mucus: 3 }, + { temperature: 36.55, mucus: 3 }, + { temperature: 36.45, mucus: 3 }, + { temperature: 36.5, mucus: 4 }, + { temperature: 36.55, mucus: 4 }, + { temperature: 36.7, mucus: 3 }, + { temperature: 36.65, mucus: 3 }, + { temperature: 36.75, mucus: 4 }, + { temperature: 36.8, mucus: 4 }, + { temperature: 36.85, mucus: 4 }, + { temperature: 36.8, mucus: 4 }, + { temperature: 36.9, mucus: 4 } +].map(convertToSymptoFormat) + +const cycleWithoutAnyShifts = [ + { temperature: 36.6, bleeding: 2 }, + { temperature: 36.65 }, + { temperature: 36.5 }, + { temperature: 36.6 }, + { temperature: 36.55 }, + { temperature: 36.7, mucus: 0 }, + { temperature: 36.75, mucus: 0 }, + { temperature: 36.45, mucus: 1 } +].map(convertToSymptoFormat) + +export { + cycleWithoutTempShift, + cycleWithTempAndMucusShift, + cycleWithTempAndNoMucusShift, + cycleWithTempShift, + cycleWithoutAnyShifts +} \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index 0f02976..e3cd067 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -1,99 +1,17 @@ import chai from 'chai' import getSensiplanStatus from '../../lib/sympto' +import { + cycleWithoutTempShift, cycleWithTempAndMucusShift, cycleWithTempAndNoMucusShift, cycleWithTempShift, cycleWithoutAnyShifts +} from './fixtures' const expect = chai.expect -function convertToSymptoFormat(val, i) { - ++i - const dayString = i < 10 ? `0${i}` : i - const sympto = { date: `2018-06-${dayString}` } - if (val.temperature) sympto.temperature = { value: val.temperature } - if (val.mucus) sympto.mucus = { value: val.mucus } - if (val.bleeding) sympto.bleeding = { value: val.bleeding } - return sympto -} - -const cycleWithTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8, 36.8] - .map(num => ({ temperature: num })) - .map(convertToSymptoFormat) - -const cycleWithoutTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8] - .map(num => ({ temperature: num })) - .map(convertToSymptoFormat) - -const cycleWithTempAndMucusShift = [ - { temperature: 36.6, bleeding: 2 }, - { temperature: 36.65 }, - { temperature: 36.5 }, - { temperature: 36.6 }, - { temperature: 36.55 }, - { temperature: 36.7, mucus: 0 }, - { temperature: 36.75, mucus: 0 }, - { temperature: 36.45, mucus: 1 }, - { temperature: 36.5, mucus: 4 }, - { temperature: 36.4, mucus: 2 }, - { temperature: 36.5, mucus: 3 }, - { temperature: 36.55, mucus: 3 }, - { temperature: 36.45, mucus: 3 }, - { temperature: 36.5, mucus: 4 }, - { temperature: 36.55, mucus: 4 }, - { temperature: 36.7, mucus: 3 }, - { temperature: 36.65, mucus: 3 }, - { temperature: 36.75, mucus: 4 }, - { temperature: 36.8, mucus: 1 }, - { temperature: 36.85, mucus: 2 }, - { temperature: 36.8, mucus: 2 }, - { temperature: 36.9, mucus: 2 }, - { temperature: 36.9, mucus: 1 }, - { temperature: 36.85, mucus: 1 }, - { temperature: 36.9, mucus: 1 }, - { temperature: 36.8, mucus: 1 }, - { temperature: 36.9, mucus: 1 } -].map(convertToSymptoFormat) - -const cycleWithTempAndNoMucusShift = [ - { temperature: 36.6, bleeding: 2 }, - { temperature: 36.65 }, - { temperature: 36.5 }, - { temperature: 36.6 }, - { temperature: 36.55 }, - { temperature: 36.7, mucus: 0 }, - { temperature: 36.75, mucus: 0 }, - { temperature: 36.45, mucus: 1 }, - { temperature: 36.5, mucus: 4 }, - { temperature: 36.4, mucus: 2 }, - { temperature: 36.5, mucus: 3 }, - { temperature: 36.55, mucus: 3 }, - { temperature: 36.45, mucus: 3 }, - { temperature: 36.5, mucus: 4 }, - { temperature: 36.55, mucus: 4 }, - { temperature: 36.7, mucus: 3 }, - { temperature: 36.65, mucus: 3 }, - { temperature: 36.75, mucus: 4 }, - { temperature: 36.8, mucus: 4 }, - { temperature: 36.85, mucus: 4 }, - { temperature: 36.8, mucus: 4 }, - { temperature: 36.9, mucus: 4 } -].map(convertToSymptoFormat) - describe('sympto', () => { describe('evaluating mucus and temperature shift together', () => { describe('with no previous higher measurement', () => { it('with no shifts detects only peri-ovulatory', function () { - const values = [ - { temperature: 36.6, bleeding: 2 }, - { temperature: 36.65 }, - { temperature: 36.5 }, - { temperature: 36.6 }, - { temperature: 36.55 }, - { temperature: 36.7, mucus: 0 }, - { temperature: 36.75, mucus: 0 }, - { temperature: 36.45, mucus: 1 } - ] - - const cycle = values.map(convertToSymptoFormat) const status = getSensiplanStatus({ - cycle, + cycle: cycleWithoutAnyShifts, previousCycle: cycleWithoutTempShift }) @@ -102,7 +20,7 @@ describe('sympto', () => { phases: { periOvulatory: { start: { date: '2018-06-01' }, - cycleDays: cycle + cycleDays: cycleWithoutAnyShifts } }, }) From 0de009335fc4e7b10d74c2c1b4f2cc567a5bbca5 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 10 Jul 2018 13:22:34 +0200 Subject: [PATCH 27/59] Handle pre-ovulatory = current --- lib/sympto/index.js | 18 ++++++++++-------- test/sympto/fixtures.js | 11 ++++++++++- test/sympto/index.spec.js | 19 ++++++++++++++++++- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 2d2f244..e32f8a2 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -7,23 +7,25 @@ export default function ({ cycle, previousCycle }) { // TODO check for basic stuff, throw if nonexistent const status = { assumeFertility: true, - phases: { - periOvulatory: { - start: { - date: null - }, - cycleDays: null - } - } + phases: {} } // if there was no first higher measurement in the previous cycle, // no infertile pre-ovulatory phase may be assumed if (getTemperatureShift(previousCycle).detected && !cycle[0].mucus) { status.phases.preOvulatory = getPreOvulatoryPhase(cycle) + if (status.phases.preOvulatory.cycleDays.length === cycle.length) { + status.assumeFertility = false + return status + } } + status.phases.periOvulatory = { + start: { date: null }, + cycleDays: [] + } const periPhase = status.phases.periOvulatory + if (status.phases.preOvulatory) { const prePhase = status.phases.preOvulatory periPhase.start.date = LocalDate.parse(prePhase.end.date).plusDays(1).toString() diff --git a/test/sympto/fixtures.js b/test/sympto/fixtures.js index 14d1eb2..159c055 100644 --- a/test/sympto/fixtures.js +++ b/test/sympto/fixtures.js @@ -83,10 +83,19 @@ const cycleWithoutAnyShifts = [ { temperature: 36.45, mucus: 1 } ].map(convertToSymptoFormat) +const fiveDayCycle = [ + { temperature: 36.6, bleeding: 2 }, + { temperature: 36.65 }, + { temperature: 36.5 }, + { temperature: 36.6 }, + { temperature: 36.55 } +].map(convertToSymptoFormat) + export { cycleWithoutTempShift, cycleWithTempAndMucusShift, cycleWithTempAndNoMucusShift, cycleWithTempShift, - cycleWithoutAnyShifts + cycleWithoutAnyShifts, + fiveDayCycle } \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index e3cd067..b223629 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -1,7 +1,7 @@ import chai from 'chai' import getSensiplanStatus from '../../lib/sympto' import { - cycleWithoutTempShift, cycleWithTempAndMucusShift, cycleWithTempAndNoMucusShift, cycleWithTempShift, cycleWithoutAnyShifts + cycleWithoutTempShift, cycleWithTempAndMucusShift, cycleWithTempAndNoMucusShift, cycleWithTempShift, cycleWithoutAnyShifts, fiveDayCycle } from './fixtures' const expect = chai.expect @@ -52,6 +52,23 @@ describe('sympto', () => { }) }) describe('with previous higher measurement', () => { + describe('with no shifts detects pre-ovulatory phase', function () { + it('according to 5-day-rule', function () { + const status = getSensiplanStatus({ + cycle: fiveDayCycle, + previousCycle: cycleWithTempShift + }) + + expect(Object.keys(status.phases).length).to.eql(1) + expect(status.assumeFertility).to.be.false() + expect(status.phases.preOvulatory).to.eql({ + cycleDays: fiveDayCycle, + start: { date: '2018-06-01' }, + end: { date: '2018-06-05' } + }) + }) + + }) describe('with no shifts detects pre- and peri-ovulatory phase', function () { it('according to 5-day-rule', function () { const status = getSensiplanStatus({ From 81b7b46b238e6b65ca925ca57e9682d9e0004c16 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 10 Jul 2018 14:22:40 +0200 Subject: [PATCH 28/59] Use explicit dates with gaps and fix bugs that this uncovers --- lib/sympto/pre-ovulatory.js | 8 ++- test/sympto/fixtures.js | 121 ++++++++++++++++-------------------- test/sympto/index.spec.js | 14 ++--- 3 files changed, 66 insertions(+), 77 deletions(-) diff --git a/lib/sympto/pre-ovulatory.js b/lib/sympto/pre-ovulatory.js index 634b410..5aa3c6a 100644 --- a/lib/sympto/pre-ovulatory.js +++ b/lib/sympto/pre-ovulatory.js @@ -1,5 +1,9 @@ +import { LocalDate } from "js-joda" + export default function(cycle) { - const fiveDayRuleDays = cycle.slice(0, 5) + const startDate = LocalDate.parse(cycle[0].date) + const fiveDayEndDate = startDate.plusDays(4).toString() + const fiveDayRuleDays = cycle.slice(0, 5).filter(d => d.date <= fiveDayEndDate) const preOvulatoryDays = getDaysUntilFertileMucus(fiveDayRuleDays) return { cycleDays: preOvulatoryDays, @@ -7,7 +11,7 @@ export default function(cycle) { date: preOvulatoryDays[0].date }, end: { - date: preOvulatoryDays[preOvulatoryDays.length - 1].date, + date: fiveDayEndDate } } } diff --git a/test/sympto/fixtures.js b/test/sympto/fixtures.js index 159c055..a7b237d 100644 --- a/test/sympto/fixtures.js +++ b/test/sympto/fixtures.js @@ -1,8 +1,6 @@ -function convertToSymptoFormat(val, i) { - ++i - const dayString = i < 10 ? `0${i}` : i - const sympto = { date: `2018-06-${dayString}` } +function convertToSymptoFormat(val) { + const sympto = { date: val.date } if (val.temperature) sympto.temperature = { value: val.temperature } if (val.mucus) sympto.mucus = { value: val.mucus } if (val.bleeding) sympto.bleeding = { value: val.bleeding } @@ -10,85 +8,72 @@ function convertToSymptoFormat(val, i) { } const cycleWithTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8, 36.8] - .map(num => ({ temperature: num })) + .map(num => ({ date: '2018-06-01', temperature: num })) .map(convertToSymptoFormat) const cycleWithoutTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8] - .map(num => ({ temperature: num })) + .map(num => ({ date: '2018-06-01', temperature: num })) .map(convertToSymptoFormat) const cycleWithTempAndMucusShift = [ - { temperature: 36.6, bleeding: 2 }, - { temperature: 36.65 }, - { temperature: 36.5 }, - { temperature: 36.6 }, - { temperature: 36.55 }, - { temperature: 36.7, mucus: 0 }, - { temperature: 36.75, mucus: 0 }, - { temperature: 36.45, mucus: 1 }, - { temperature: 36.5, mucus: 4 }, - { temperature: 36.4, mucus: 2 }, - { temperature: 36.5, mucus: 3 }, - { temperature: 36.55, mucus: 3 }, - { temperature: 36.45, mucus: 3 }, - { temperature: 36.5, mucus: 4 }, - { temperature: 36.55, mucus: 4 }, - { temperature: 36.7, mucus: 3 }, - { temperature: 36.65, mucus: 3 }, - { temperature: 36.75, mucus: 4 }, - { temperature: 36.8, mucus: 1 }, - { temperature: 36.85, mucus: 2 }, - { temperature: 36.8, mucus: 2 }, - { temperature: 36.9, mucus: 2 }, - { temperature: 36.9, mucus: 1 }, - { temperature: 36.85, mucus: 1 }, - { temperature: 36.9, mucus: 1 }, - { temperature: 36.8, mucus: 1 }, - { temperature: 36.9, mucus: 1 } + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-09', temperature: 36.5, mucus: 4 }, + { date: '2018-06-10', temperature: 36.4, mucus: 2 }, + { date: '2018-06-13', temperature: 36.45, mucus: 3 }, + { date: '2018-06-14', temperature: 36.5, mucus: 4 }, + { date: '2018-06-15', temperature: 36.55, mucus: 4 }, + { date: '2018-06-16', temperature: 36.7, mucus: 3 }, + { date: '2018-06-17', temperature: 36.65, mucus: 3 }, + { date: '2018-06-18', temperature: 36.75, mucus: 4 }, + { date: '2018-06-19', temperature: 36.8, mucus: 1 }, + { date: '2018-06-20', temperature: 36.85, mucus: 2 }, + { date: '2018-06-21', temperature: 36.8, mucus: 2 }, + { date: '2018-06-22', temperature: 36.9, mucus: 2 }, + { date: '2018-06-25', temperature: 36.9, mucus: 1 }, + { date: '2018-06-26', temperature: 36.8, mucus: 1 }, + { date: '2018-06-27', temperature: 36.9, mucus: 1 } ].map(convertToSymptoFormat) const cycleWithTempAndNoMucusShift = [ - { temperature: 36.6, bleeding: 2 }, - { temperature: 36.65 }, - { temperature: 36.5 }, - { temperature: 36.6 }, - { temperature: 36.55 }, - { temperature: 36.7, mucus: 0 }, - { temperature: 36.75, mucus: 0 }, - { temperature: 36.45, mucus: 1 }, - { temperature: 36.5, mucus: 4 }, - { temperature: 36.4, mucus: 2 }, - { temperature: 36.5, mucus: 3 }, - { temperature: 36.55, mucus: 3 }, - { temperature: 36.45, mucus: 3 }, - { temperature: 36.5, mucus: 4 }, - { temperature: 36.55, mucus: 4 }, - { temperature: 36.7, mucus: 3 }, - { temperature: 36.65, mucus: 3 }, - { temperature: 36.75, mucus: 4 }, - { temperature: 36.8, mucus: 4 }, - { temperature: 36.85, mucus: 4 }, - { temperature: 36.8, mucus: 4 }, - { temperature: 36.9, mucus: 4 } + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-08', temperature: 36.45, mucus: 1 }, + { date: '2018-06-09', temperature: 36.5, mucus: 4 }, + { date: '2018-06-10', temperature: 36.4, mucus: 2 }, + { date: '2018-06-11', temperature: 36.5, mucus: 3 }, + { date: '2018-06-13', temperature: 36.45, mucus: 3 }, + { date: '2018-06-14', temperature: 36.5, mucus: 4 }, + { date: '2018-06-15', temperature: 36.55, mucus: 4 }, + { date: '2018-06-16', temperature: 36.7, mucus: 3 }, + { date: '2018-06-17', temperature: 36.65, mucus: 3 }, + { date: '2018-06-18', temperature: 36.75, mucus: 4 }, + { date: '2018-06-19', temperature: 36.8, mucus: 4 }, + { date: '2018-06-20', temperature: 36.85, mucus: 4 }, + { date: '2018-06-23', temperature: 36.9, mucus: 3 }, + { date: '2018-06-24', temperature: 36.85, mucus: 4 }, + { date: '2018-06-26', temperature: 36.8, mucus: 4 }, + { date: '2018-06-27', temperature: 36.9, mucus: 4 } ].map(convertToSymptoFormat) const cycleWithoutAnyShifts = [ - { temperature: 36.6, bleeding: 2 }, - { temperature: 36.65 }, - { temperature: 36.5 }, - { temperature: 36.6 }, - { temperature: 36.55 }, - { temperature: 36.7, mucus: 0 }, - { temperature: 36.75, mucus: 0 }, - { temperature: 36.45, mucus: 1 } + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-07', temperature: 36.75, mucus: 0 }, + { date: '2018-06-08', temperature: 36.45, mucus: 1 } ].map(convertToSymptoFormat) const fiveDayCycle = [ - { temperature: 36.6, bleeding: 2 }, - { temperature: 36.65 }, - { temperature: 36.5 }, - { temperature: 36.6 }, - { temperature: 36.55 } + { date: '2018-06-01', bleeding: 2 }, + { date: '2018-06-03', bleeding: 3 }, ].map(convertToSymptoFormat) export { diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index b223629..46d234f 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -39,14 +39,14 @@ describe('sympto', () => { expect(status.phases.periOvulatory).to.eql({ start: { date: '2018-06-01' }, end: { date: '2018-06-21', time: '18:00' }, - cycleDays: cycleWithTempAndMucusShift.slice(0, 21) + cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date <= '2018-06-21') }) expect(status.phases.postOvulatory).to.eql({ start: { date: '2018-06-21', time: '18:00' }, - cycleDays: cycleWithTempAndMucusShift.slice(20) + cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date >= '2018-06-21') }) }) }) @@ -79,12 +79,12 @@ describe('sympto', () => { expect(Object.keys(status.phases).length).to.eql(2) expect(status.assumeFertility).to.be.true() expect(status.phases.preOvulatory).to.eql({ - cycleDays: cycleWithTempAndNoMucusShift.slice(0,5), + cycleDays: cycleWithTempAndNoMucusShift.filter(({date}) => date <= '2018-06-05'), start: { date: '2018-06-01' }, end: { date: '2018-06-05' } }) expect(status.phases.periOvulatory).to.eql({ - cycleDays: cycleWithTempAndNoMucusShift.slice(5), + cycleDays: cycleWithTempAndNoMucusShift.filter(({date}) => date > '2018-06-05'), start: { date: '2018-06-06' } }) }) @@ -100,17 +100,17 @@ describe('sympto', () => { expect(Object.keys(status.phases).length).to.eql(3) expect(status.assumeFertility).to.be.false() expect(status.phases.preOvulatory).to.eql({ - cycleDays: cycleWithTempAndMucusShift.slice(0,5), + cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date <= '2018-06-05'), start: { date: '2018-06-01' }, end: { date: '2018-06-05' } }) expect(status.phases.periOvulatory).to.eql({ - cycleDays: cycleWithTempAndMucusShift.slice(5, 21), + cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date > '2018-06-05' && date <= '2018-06-21'), start: { date: '2018-06-06' }, end: { date: '2018-06-21', time: '18:00'} }) expect(status.phases.postOvulatory).to.eql({ - cycleDays: cycleWithTempAndMucusShift.slice(20), + cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date >= '2018-06-21'), start: { date: '2018-06-21', time: '18:00'} }) }) From d027c6e9c45703deb4dab46f965a9482b5f83838 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 10 Jul 2018 16:37:38 +0200 Subject: [PATCH 29/59] Throw if args are wrong --- lib/sympto/index.js | 25 +++++++- test/sympto/fixtures.js | 2 + test/sympto/index.spec.js | 131 +++++++++++++++++++++++++++----------- 3 files changed, 119 insertions(+), 39 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index e32f8a2..291c378 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -4,7 +4,8 @@ import getPreOvulatoryPhase from './pre-ovulatory' import { LocalDate } from 'js-joda' export default function ({ cycle, previousCycle }) { - // TODO check for basic stuff, throw if nonexistent + throwIfArgsAreNotInRequiredFormat(cycle, previousCycle) + const status = { assumeFertility: true, phases: {} @@ -66,4 +67,26 @@ export default function ({ cycle, previousCycle }) { status.assumeFertility = false return status +} + +function throwIfArgsAreNotInRequiredFormat(cycle, previousCycle) { + if (!Array.isArray(cycle) || !cycle.length) throw new Error('Please provide all cycle days as array') + if (!Array.isArray(previousCycle) || !previousCycle.length) throw new Error('Please provide previous cycle days as array') + if ( + !cycle[0].bleeding || typeof cycle[0].bleeding.value != 'number' || + !previousCycle[0].bleeding || typeof previousCycle[0].bleeding.value != 'number' + ) throw new Error('Cycle must start with bleeding') + + if ([cycle, previousCycle].some(cycle => { + return cycle.some(day => { + if (!day.date) return true + try { + LocalDate.parse(day.date) + } catch(err) { + throw new Error('Please provide dates in ISO8601 format') + } + if (day.temperature && typeof day.temperature.value != 'number') return true + if (day.mucus && typeof day.mucus.value != 'number') return true + }) + })) throw new Error('Cycle days are not in correct format') } \ No newline at end of file diff --git a/test/sympto/fixtures.js b/test/sympto/fixtures.js index a7b237d..4304f82 100644 --- a/test/sympto/fixtures.js +++ b/test/sympto/fixtures.js @@ -10,10 +10,12 @@ function convertToSymptoFormat(val) { const cycleWithTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8, 36.8] .map(num => ({ date: '2018-06-01', temperature: num })) .map(convertToSymptoFormat) +cycleWithTempShift.unshift({date: '2018-05-30', bleeding: { value: 2 }}) const cycleWithoutTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8] .map(num => ({ date: '2018-06-01', temperature: num })) .map(convertToSymptoFormat) +cycleWithoutTempShift.unshift({date: '2018-05-30', bleeding: { value: 2 }}) const cycleWithTempAndMucusShift = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index 46d234f..e950280 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -7,47 +7,45 @@ import { const expect = chai.expect describe('sympto', () => { - describe('evaluating mucus and temperature shift together', () => { - describe('with no previous higher measurement', () => { - it('with no shifts detects only peri-ovulatory', function () { - const status = getSensiplanStatus({ - cycle: cycleWithoutAnyShifts, - previousCycle: cycleWithoutTempShift - }) - - expect(status).to.eql({ - assumeFertility: true, - phases: { - periOvulatory: { - start: { date: '2018-06-01' }, - cycleDays: cycleWithoutAnyShifts - } - }, - }) + describe('with no previous higher measurement', () => { + it('with no shifts detects only peri-ovulatory', function () { + const status = getSensiplanStatus({ + cycle: cycleWithoutAnyShifts, + previousCycle: cycleWithoutTempShift }) - it('with shifts detects only peri-ovulatory and post-ovulatory', function () { - const status = getSensiplanStatus({ - cycle: cycleWithTempAndMucusShift, - previousCycle: cycleWithoutTempShift - }) + expect(status).to.eql({ + assumeFertility: true, + phases: { + periOvulatory: { + start: { date: '2018-06-01' }, + cycleDays: cycleWithoutAnyShifts + } + }, + }) + }) - expect(status.temperatureShift).to.be.an('object') - expect(status.mucusShift).to.be.an('object') - expect(status.assumeFertility).to.be.false() - expect(Object.keys(status.phases).length).to.eql(2) - expect(status.phases.periOvulatory).to.eql({ - start: { date: '2018-06-01' }, - end: { date: '2018-06-21', time: '18:00' }, - cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date <= '2018-06-21') - }) - expect(status.phases.postOvulatory).to.eql({ - start: { - date: '2018-06-21', - time: '18:00' - }, - cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date >= '2018-06-21') - }) + it('with shifts detects only peri-ovulatory and post-ovulatory', function () { + const status = getSensiplanStatus({ + cycle: cycleWithTempAndMucusShift, + previousCycle: cycleWithoutTempShift + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(2) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-21', time: '18:00' }, + cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date <= '2018-06-21') + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-21', + time: '18:00' + }, + cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date >= '2018-06-21') }) }) }) @@ -117,4 +115,61 @@ describe('sympto', () => { }) }) + describe('when args are wrong', () => { + it('throws when arg object is not in right format', () => { + const wrongObject = { hello: 'world' } + expect(() => getSensiplanStatus(wrongObject)).to.throw('cycle days as array') + }) + it('throws if cycle array is empty', () => { + expect(() => getSensiplanStatus({cycle: []})).to.throw('cycle days as array') + }) + it('throws if cycle days are not in right format', () => { + expect(() => getSensiplanStatus({ + cycle: [{ + hello: 'world', + bleeding: { value: 0 } + }], + previousCycle: [{ + date: '1992-09-09', + bleeding: { value: 0 } + }] + })).to.throw('correct format') + expect(() => getSensiplanStatus({ + cycle: [{ + date: '2018-04-13', + temperature: {value: '35'}, + bleeding: { value: 0 } + }], + previousCycle: [{ + date: '1992-09-09', + bleeding: { value: 0 } + }] + })).to.throw('correct format') + expect(() => getSensiplanStatus({ + cycle: [{ + date: '09-14-2017', + bleeding: { value: 0 } + }], + previousCycle: [{ + date: '1992-09-09', + bleeding: { value: 0 } + }] + })).to.throw('ISO') + }) + it('throws if first cycle day does not have bleeding value', () => { + expect(() => getSensiplanStatus({ + cycle: [{ + date: '2017-01-01', + bleeding: { + value: 'medium' + } + }], + previousCycle: [ + { + date: '2017-09-23', + } + ] + })).to.throw('start with bleeding') + }) + }) }) \ No newline at end of file From 60c61894666aa5128954e5b1046293a5dd0916ba Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 10 Jul 2018 17:04:54 +0200 Subject: [PATCH 30/59] Shorten pre-ovu phase when early mucus --- lib/sympto/pre-ovulatory.js | 9 ++++++++- test/sympto/fixtures.js | 26 +++++++++++++++++++++++++- test/sympto/index.spec.js | 25 ++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/lib/sympto/pre-ovulatory.js b/lib/sympto/pre-ovulatory.js index 5aa3c6a..8cc86b9 100644 --- a/lib/sympto/pre-ovulatory.js +++ b/lib/sympto/pre-ovulatory.js @@ -5,13 +5,20 @@ export default function(cycle) { const fiveDayEndDate = startDate.plusDays(4).toString() const fiveDayRuleDays = cycle.slice(0, 5).filter(d => d.date <= fiveDayEndDate) const preOvulatoryDays = getDaysUntilFertileMucus(fiveDayRuleDays) + let endDate + if (preOvulatoryDays.length === fiveDayRuleDays.length) { + endDate = fiveDayEndDate + } else { + endDate = preOvulatoryDays[preOvulatoryDays.length - 1].date + } + return { cycleDays: preOvulatoryDays, start: { date: preOvulatoryDays[0].date }, end: { - date: fiveDayEndDate + date: endDate } } } diff --git a/test/sympto/fixtures.js b/test/sympto/fixtures.js index 4304f82..45ab578 100644 --- a/test/sympto/fixtures.js +++ b/test/sympto/fixtures.js @@ -63,6 +63,29 @@ const cycleWithTempAndNoMucusShift = [ { date: '2018-06-27', temperature: 36.9, mucus: 4 } ].map(convertToSymptoFormat) +const cycleWithEarlyMucus = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65, mucus: 3 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-08', temperature: 36.45, mucus: 1 }, + { date: '2018-06-09', temperature: 36.5, mucus: 4 }, + { date: '2018-06-10', temperature: 36.4, mucus: 2 }, + { date: '2018-06-11', temperature: 36.5, mucus: 3 }, + { date: '2018-06-13', temperature: 36.45, mucus: 3 }, + { date: '2018-06-14', temperature: 36.5, mucus: 4 }, + { date: '2018-06-15', temperature: 36.55, mucus: 4 }, + { date: '2018-06-16', temperature: 36.7, mucus: 3 }, + { date: '2018-06-17', temperature: 36.65, mucus: 3 }, + { date: '2018-06-18', temperature: 36.75, mucus: 4 }, + { date: '2018-06-19', temperature: 36.8, mucus: 4 }, + { date: '2018-06-20', temperature: 36.85, mucus: 4 }, + { date: '2018-06-23', temperature: 36.9, mucus: 3 }, + { date: '2018-06-24', temperature: 36.85, mucus: 4 }, + { date: '2018-06-26', temperature: 36.8, mucus: 4 }, + { date: '2018-06-27', temperature: 36.9, mucus: 4 } +].map(convertToSymptoFormat) + const cycleWithoutAnyShifts = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65 }, @@ -84,5 +107,6 @@ export { cycleWithTempAndNoMucusShift, cycleWithTempShift, cycleWithoutAnyShifts, - fiveDayCycle + fiveDayCycle, + cycleWithEarlyMucus } \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index e950280..6bc56e7 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -1,7 +1,13 @@ import chai from 'chai' import getSensiplanStatus from '../../lib/sympto' import { - cycleWithoutTempShift, cycleWithTempAndMucusShift, cycleWithTempAndNoMucusShift, cycleWithTempShift, cycleWithoutAnyShifts, fiveDayCycle + cycleWithoutTempShift, + cycleWithTempAndMucusShift, + cycleWithTempAndNoMucusShift, + cycleWithTempShift, + cycleWithoutAnyShifts, + fiveDayCycle, + cycleWithEarlyMucus } from './fixtures' const expect = chai.expect @@ -86,7 +92,24 @@ describe('sympto', () => { start: { date: '2018-06-06' } }) }) + it('according to 5-day-rule with shortened pre-phase', function () { + const status = getSensiplanStatus({ + cycle: cycleWithEarlyMucus, + previousCycle: cycleWithTempShift + }) + expect(Object.keys(status.phases).length).to.eql(2) + expect(status.assumeFertility).to.be.true() + expect(status.phases.preOvulatory).to.eql({ + cycleDays: [cycleWithEarlyMucus[0]], + start: { date: '2018-06-01' }, + end: { date: '2018-06-01' } + }) + expect(status.phases.periOvulatory).to.eql({ + cycleDays: cycleWithEarlyMucus.slice(1), + start: { date: '2018-06-02' } + }) + }) }) describe('with shifts detects pre- and peri-ovulatory phase', function () { it('according to 5-day-rule', function () { From a1306cd7c5ec124ad161771bd59fc2dcdd6afed9 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 10 Jul 2018 17:18:50 +0200 Subject: [PATCH 31/59] Use assert for args check --- lib/sympto/index.js | 30 ++++++++++++------------------ test/sympto/index.spec.js | 13 +++++++------ 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 291c378..6a9f66a 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -2,6 +2,7 @@ import getTemperatureShift from './temperature' import getMucusShift from './mucus' import getPreOvulatoryPhase from './pre-ovulatory' import { LocalDate } from 'js-joda' +import assert from 'assert' export default function ({ cycle, previousCycle }) { throwIfArgsAreNotInRequiredFormat(cycle, previousCycle) @@ -70,23 +71,16 @@ export default function ({ cycle, previousCycle }) { } function throwIfArgsAreNotInRequiredFormat(cycle, previousCycle) { - if (!Array.isArray(cycle) || !cycle.length) throw new Error('Please provide all cycle days as array') - if (!Array.isArray(previousCycle) || !previousCycle.length) throw new Error('Please provide previous cycle days as array') - if ( - !cycle[0].bleeding || typeof cycle[0].bleeding.value != 'number' || - !previousCycle[0].bleeding || typeof previousCycle[0].bleeding.value != 'number' - ) throw new Error('Cycle must start with bleeding') - - if ([cycle, previousCycle].some(cycle => { - return cycle.some(day => { - if (!day.date) return true - try { - LocalDate.parse(day.date) - } catch(err) { - throw new Error('Please provide dates in ISO8601 format') - } - if (day.temperature && typeof day.temperature.value != 'number') return true - if (day.mucus && typeof day.mucus.value != 'number') return true + [cycle, previousCycle].forEach(cycle => { + assert.ok(Array.isArray(cycle)) + assert.ok(cycle.length > 0) + assert.equal(typeof cycle[0].bleeding, 'object') + assert.equal(typeof cycle[0].bleeding.value, 'number') + cycle.forEach(day => { + assert.equal(typeof day.date, 'string') + assert.doesNotThrow(() => LocalDate.parse(day.date)) + if (day.temperature) assert.equal(typeof day.temperature.value, 'number') + if (day.mucus) assert.equal(typeof day.mucus.value, 'number') }) - })) throw new Error('Cycle days are not in correct format') + }) } \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index 6bc56e7..5a9bc38 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -1,5 +1,6 @@ import chai from 'chai' import getSensiplanStatus from '../../lib/sympto' +import { AssertionError } from 'assert' import { cycleWithoutTempShift, cycleWithTempAndMucusShift, @@ -141,10 +142,10 @@ describe('sympto', () => { describe('when args are wrong', () => { it('throws when arg object is not in right format', () => { const wrongObject = { hello: 'world' } - expect(() => getSensiplanStatus(wrongObject)).to.throw('cycle days as array') + expect(() => getSensiplanStatus(wrongObject)).to.throw(AssertionError) }) it('throws if cycle array is empty', () => { - expect(() => getSensiplanStatus({cycle: []})).to.throw('cycle days as array') + expect(() => getSensiplanStatus({cycle: []})).to.throw(AssertionError) }) it('throws if cycle days are not in right format', () => { expect(() => getSensiplanStatus({ @@ -156,7 +157,7 @@ describe('sympto', () => { date: '1992-09-09', bleeding: { value: 0 } }] - })).to.throw('correct format') + })).to.throw(AssertionError) expect(() => getSensiplanStatus({ cycle: [{ date: '2018-04-13', @@ -167,7 +168,7 @@ describe('sympto', () => { date: '1992-09-09', bleeding: { value: 0 } }] - })).to.throw('correct format') + })).to.throw(AssertionError) expect(() => getSensiplanStatus({ cycle: [{ date: '09-14-2017', @@ -177,7 +178,7 @@ describe('sympto', () => { date: '1992-09-09', bleeding: { value: 0 } }] - })).to.throw('ISO') + })).to.throw(AssertionError) }) it('throws if first cycle day does not have bleeding value', () => { expect(() => getSensiplanStatus({ @@ -192,7 +193,7 @@ describe('sympto', () => { date: '2017-09-23', } ] - })).to.throw('start with bleeding') + })).to.throw(AssertionError) }) }) }) \ No newline at end of file From 9e645ddd8ec6918e5cdf01e43a209c96dc4d1c5f Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Wed, 11 Jul 2018 13:00:16 +0200 Subject: [PATCH 32/59] Add more tests for combinations of fhm and mucus peak --- lib/sympto/mucus.js | 14 ++-- test/sympto/fixtures.js | 119 ++++++++++++++++++++++++++- test/sympto/index.spec.js | 165 +++++++++++++++++++++++++++++++++++++- 3 files changed, 290 insertions(+), 8 deletions(-) diff --git a/lib/sympto/mucus.js b/lib/sympto/mucus.js index f848881..07a7ca4 100644 --- a/lib/sympto/mucus.js +++ b/lib/sympto/mucus.js @@ -12,23 +12,25 @@ export default function (cycleDays, tempEvalEndIndex) { const threeFollowingDays = mucusDays.slice(i + 1, i + 4) if (threeFollowingDays.length < 3) continue - const bestQualityOccurringIn3FollowingDays = threeFollowingDays.some(day => day.mucus.value >= bestQuality) + const bestQualityOccurringIn3FollowingDays = threeFollowingDays.some(day => { + return day.mucus.value >= bestQuality + }) if (bestQualityOccurringIn3FollowingDays) continue - // FIXME mucus peak can be same day as first higher measurement const cycleDayIndex = cycleDays.indexOf(day) const relevantDays = cycleDays .slice(cycleDayIndex + 1, tempEvalEndIndex + 1) .filter(day => day.mucus && !day.mucus.exclude) - const evaluationCompleteDay = relevantDays.length > 3 ? - relevantDays[relevantDays.length - 1] : threeFollowingDays[threeFollowingDays.length - 1] + const noBestQualityUntilEndOfTempEval = relevantDays.every(day => { + return day.mucus.value < bestQuality + }) - if (relevantDays.every(day => day.mucus.value < bestQuality)) { + if (noBestQualityUntilEndOfTempEval) { return { detected: true, mucusPeak: day, - evaluationCompleteDay + evaluationCompleteDay: threeFollowingDays[threeFollowingDays.length - 1] } } } diff --git a/test/sympto/fixtures.js b/test/sympto/fixtures.js index 45ab578..1616ed4 100644 --- a/test/sympto/fixtures.js +++ b/test/sympto/fixtures.js @@ -101,6 +101,118 @@ const fiveDayCycle = [ { date: '2018-06-03', bleeding: 3 }, ].map(convertToSymptoFormat) +const mucusPeakAndFhmOnSameDay = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-09', temperature: 36.5, mucus: 4 }, + { date: '2018-06-10', temperature: 36.4, mucus: 2 }, + { date: '2018-06-13', temperature: 36.45, mucus: 3 }, + { date: '2018-06-14', temperature: 36.5, mucus: 4 }, + { date: '2018-06-15', temperature: 36.55, mucus: 4 }, + { date: '2018-06-16', temperature: 36.7, mucus: 3 }, + { date: '2018-06-17', temperature: 36.65, mucus: 3 }, + { date: '2018-06-18', temperature: 36.75, mucus: 4 }, + { date: '2018-06-19', temperature: 36.8, mucus: 3 }, + { date: '2018-06-20', temperature: 36.9, mucus: 2 }, + { date: '2018-06-21', temperature: 36.8, mucus: 2 }, + { date: '2018-06-22', temperature: 36.9, mucus: 2 }, + { date: '2018-06-25', temperature: 36.9, mucus: 1 }, + { date: '2018-06-26', temperature: 36.8, mucus: 1 }, + { date: '2018-06-27', temperature: 36.9, mucus: 1 } +].map(convertToSymptoFormat) + +const fhmTwoDaysBeforeMucusPeak = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-09', temperature: 36.5, mucus: 1 }, + { date: '2018-06-10', temperature: 36.4, mucus: 2 }, + { date: '2018-06-13', temperature: 36.45, mucus: 2 }, + { date: '2018-06-14', temperature: 36.5, mucus: 2 }, + { date: '2018-06-15', temperature: 36.55, mucus: 1 }, + { date: '2018-06-16', temperature: 36.7, mucus: 2 }, + { date: '2018-06-17', temperature: 36.65, mucus: 2 }, + { date: '2018-06-18', temperature: 36.75, mucus: 2 }, + { date: '2018-06-19', temperature: 36.8, mucus: 3 }, + { date: '2018-06-20', temperature: 36.85, mucus: 2 }, + { date: '2018-06-21', temperature: 36.8, mucus: 4 }, + { date: '2018-06-22', temperature: 36.9, mucus: 2 }, + { date: '2018-06-25', temperature: 36.9, mucus: 1 }, + { date: '2018-06-26', temperature: 36.8, mucus: 1 }, +].map(convertToSymptoFormat) + +const mucusPeakTwoDaysBeforeFhm = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55, mucus: 2 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-09', temperature: 36.5, mucus: 4 }, + { date: '2018-06-10', temperature: 36.4, mucus: 2 }, + { date: '2018-06-13', temperature: 36.45, mucus: 3 }, + { date: '2018-06-14', temperature: 36.5, mucus: 4 }, + { date: '2018-06-15', temperature: 36.55, mucus: 4 }, + { date: '2018-06-16', temperature: 36.7, mucus: 4 }, + { date: '2018-06-17', temperature: 36.65, mucus: 3 }, + { date: '2018-06-18', temperature: 36.75, mucus: 2 }, + { date: '2018-07-02', temperature: 36.8, mucus: 3 }, + { date: '2018-07-03', temperature: 36.9, mucus: 2 }, + { date: '2018-07-04', temperature: 36.8, mucus: 2 }, +].map(convertToSymptoFormat) + +const mucusPeak5DaysAfterFhm = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65, mucus: 2 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-09', temperature: 36.5, mucus: 4 }, + { date: '2018-06-10', temperature: 36.4, mucus: 2 }, + { date: '2018-06-13', temperature: 36.45, mucus: 3 }, + { date: '2018-06-14', temperature: 36.5, mucus: 3 }, + { date: '2018-06-15', temperature: 36.55, mucus: 3 }, + { date: '2018-06-16', temperature: 36.7, mucus: 3 }, + { date: '2018-06-17', temperature: 36.65, mucus: 3 }, + { date: '2018-06-18', temperature: 36.60, mucus: 2 }, + { date: '2018-06-19', temperature: 36.8, mucus: 2 }, + { date: '2018-06-20', temperature: 36.85, mucus: 2 }, + { date: '2018-06-21', temperature: 36.8, mucus: 2 }, + { date: '2018-06-22', temperature: 36.9, mucus: 2 }, + { date: '2018-06-25', temperature: 36.9, mucus: 1 }, + { date: '2018-06-26', temperature: 36.8, mucus: 4 }, + { date: '2018-06-30', temperature: 36.9, mucus: 1 }, + { date: '2018-07-01', temperature: 36.9, mucus: 1 }, + { date: '2018-07-02', temperature: 36.9, mucus: 1 } +].map(convertToSymptoFormat) + +const fhm5DaysAfterMucusPeak = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-09', temperature: 36.5, mucus: 4 }, + { date: '2018-06-10', temperature: 36.4, mucus: 2 }, + { date: '2018-06-13', temperature: 36.45, mucus: 3 }, + { date: '2018-06-14', temperature: 36.5, mucus: 4 }, + { date: '2018-06-15', temperature: 36.55, mucus: 3 }, + { date: '2018-06-16', temperature: 36.7, mucus: 3 }, + { date: '2018-06-17', temperature: 36.65, mucus: 3 }, + { date: '2018-06-18', temperature: 36.75, mucus: 2 }, + { date: '2018-06-19', temperature: 36.8, mucus: 2 }, + { date: '2018-06-20', temperature: 36.85, mucus: 2 }, + { date: '2018-06-21', temperature: 36.8, mucus: 2 }, + { date: '2018-06-22', temperature: 36.9, mucus: 2 }, + { date: '2018-06-25', temperature: 36.9, mucus: 1 }, + { date: '2018-06-26', temperature: 36.8, mucus: 4 }, + { date: '2018-06-27', temperature: 36.9, mucus: 1 } +].map(convertToSymptoFormat) + export { cycleWithoutTempShift, cycleWithTempAndMucusShift, @@ -108,5 +220,10 @@ export { cycleWithTempShift, cycleWithoutAnyShifts, fiveDayCycle, - cycleWithEarlyMucus + cycleWithEarlyMucus, + fhm5DaysAfterMucusPeak, + fhmTwoDaysBeforeMucusPeak, + mucusPeak5DaysAfterFhm, + mucusPeakAndFhmOnSameDay, + mucusPeakTwoDaysBeforeFhm } \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index 5a9bc38..ebc0106 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -8,7 +8,12 @@ import { cycleWithTempShift, cycleWithoutAnyShifts, fiveDayCycle, - cycleWithEarlyMucus + cycleWithEarlyMucus, + mucusPeakAndFhmOnSameDay, + fhmTwoDaysBeforeMucusPeak, + fhm5DaysAfterMucusPeak, + mucusPeak5DaysAfterFhm, + mucusPeakTwoDaysBeforeFhm } from './fixtures' const expect = chai.expect @@ -139,6 +144,164 @@ describe('sympto', () => { }) }) + + describe('combining first higher measurment and mucus peak', () => { + it('with fhM + mucus peak on same day finds correct start of post-ovu phase', () => { + const status = getSensiplanStatus({ + cycle: mucusPeakAndFhmOnSameDay, + previousCycle: cycleWithTempShift + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-05' }, + cycleDays: mucusPeakAndFhmOnSameDay.filter(({date}) => date <= '2018-06-05') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-06' }, + end: { date: '2018-06-21', time: '18:00' }, + cycleDays: mucusPeakAndFhmOnSameDay.filter(({date}) => { + return date > '2018-06-05' && date <= '2018-06-21' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-21', + time: '18:00' + }, + cycleDays: mucusPeakAndFhmOnSameDay.filter(({date}) => date >= '2018-06-21') + }) + }) + + it('with fhM 2 days before mucus peak waits for end of mucus eval', () => { + const status = getSensiplanStatus({ + cycle: fhmTwoDaysBeforeMucusPeak, + previousCycle: cycleWithTempShift + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-05' }, + cycleDays: fhmTwoDaysBeforeMucusPeak.filter(({date}) => date <= '2018-06-05') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-06' }, + end: { date: '2018-06-26', time: '18:00' }, + cycleDays: fhmTwoDaysBeforeMucusPeak.filter(({date}) => { + return date > '2018-06-05' && date <= '2018-06-26' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-26', + time: '18:00' + }, + cycleDays: fhmTwoDaysBeforeMucusPeak.filter(({date}) => date >= '2018-06-26') + }) + }) + + it('with another mucus peak 5 days after fHM ignores it', () => { + const status = getSensiplanStatus({ + cycle: mucusPeak5DaysAfterFhm, + previousCycle: cycleWithTempShift + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-01' }, + cycleDays: mucusPeak5DaysAfterFhm.filter(({date}) => date <= '2018-06-01') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-02' }, + end: { date: '2018-06-22', time: '18:00' }, + cycleDays: mucusPeak5DaysAfterFhm.filter(({date}) => { + return date > '2018-06-01' && date <= '2018-06-22' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-22', + time: '18:00' + }, + cycleDays: mucusPeak5DaysAfterFhm.filter(({date}) => date >= '2018-06-22') + }) + }) + + it('with mucus peak 2 days before fhM waits for end of temp eval', () => { + const status = getSensiplanStatus({ + cycle: mucusPeakTwoDaysBeforeFhm, + previousCycle: cycleWithTempShift + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-04' }, + cycleDays: mucusPeakTwoDaysBeforeFhm.filter(({date}) => date <= '2018-06-04') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-05' }, + end: { date: '2018-07-03', time: '18:00' }, + cycleDays: mucusPeakTwoDaysBeforeFhm.filter(({date}) => { + return date > '2018-06-04' && date <= '2018-07-03' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-07-03', + time: '18:00' + }, + cycleDays: mucusPeakTwoDaysBeforeFhm.filter(({date}) => date >= '2018-07-03') + }) + }) + + it('with mucus peak 5 days before fhM waits for end of temp eval', () => { + const status = getSensiplanStatus({ + cycle: fhm5DaysAfterMucusPeak, + previousCycle: cycleWithTempShift + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-05' }, + cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => date <= '2018-06-05') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-06' }, + end: { date: '2018-06-21', time: '18:00' }, + cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => { + return date > '2018-06-05' && date <= '2018-06-21' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-21', + time: '18:00' + }, + cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => date >= '2018-06-21') + }) + }) + }) + describe('when args are wrong', () => { it('throws when arg object is not in right format', () => { const wrongObject = { hello: 'world' } From 19e880183e6c39a301a03d6c324d0bc3783746b4 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Wed, 11 Jul 2018 14:14:42 +0200 Subject: [PATCH 33/59] Accept previousCycles as array --- lib/sympto/index.js | 17 +++++++++----- lib/sympto/pre-ovulatory.js | 23 ++++++++++++------ test/sympto/index.spec.js | 47 ++++++++++++++++++++++--------------- 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 6a9f66a..36f8e5e 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -4,18 +4,20 @@ import getPreOvulatoryPhase from './pre-ovulatory' import { LocalDate } from 'js-joda' import assert from 'assert' -export default function ({ cycle, previousCycle }) { - throwIfArgsAreNotInRequiredFormat(cycle, previousCycle) +export default function ({ cycle, previousCycles = [] }) { + throwIfArgsAreNotInRequiredFormat(cycle, previousCycles) const status = { assumeFertility: true, phases: {} } + // TODO handle no previous cycles // if there was no first higher measurement in the previous cycle, // no infertile pre-ovulatory phase may be assumed - if (getTemperatureShift(previousCycle).detected && !cycle[0].mucus) { - status.phases.preOvulatory = getPreOvulatoryPhase(cycle) + const lastCycle = previousCycles[previousCycles.length - 1] + if (getTemperatureShift(lastCycle).detected && !cycle[0].mucus) { + status.phases.preOvulatory = getPreOvulatoryPhase(cycle, previousCycles) if (status.phases.preOvulatory.cycleDays.length === cycle.length) { status.assumeFertility = false return status @@ -70,9 +72,10 @@ export default function ({ cycle, previousCycle }) { return status } -function throwIfArgsAreNotInRequiredFormat(cycle, previousCycle) { - [cycle, previousCycle].forEach(cycle => { +function throwIfArgsAreNotInRequiredFormat(cycle, previousCycles) { + [cycle, ...previousCycles].forEach(cycle => { assert.ok(Array.isArray(cycle)) + // TODO handle case of no previous cycles assert.ok(cycle.length > 0) assert.equal(typeof cycle[0].bleeding, 'object') assert.equal(typeof cycle[0].bleeding.value, 'number') @@ -81,6 +84,8 @@ function throwIfArgsAreNotInRequiredFormat(cycle, previousCycle) { assert.doesNotThrow(() => LocalDate.parse(day.date)) if (day.temperature) assert.equal(typeof day.temperature.value, 'number') if (day.mucus) assert.equal(typeof day.mucus.value, 'number') + if (day.mucus) assert.ok(day.mucus.value >= 0) + if (day.mucus) assert.ok(day.mucus.value < 5) }) }) } \ No newline at end of file diff --git a/lib/sympto/pre-ovulatory.js b/lib/sympto/pre-ovulatory.js index 8cc86b9..5749e1a 100644 --- a/lib/sympto/pre-ovulatory.js +++ b/lib/sympto/pre-ovulatory.js @@ -1,13 +1,20 @@ import { LocalDate } from "js-joda" -export default function(cycle) { +export default function(cycle, previousCycles) { + // TODO handle no previous cycles + let preOvuPhaseLength = 5 + + //TODO make sure it handles weird cases like fhm < 9 + const minus8DayRuleResult = apply8DayRule(previousCycles) + if (minus8DayRuleResult) preOvuPhaseLength = minus8DayRuleResult + const startDate = LocalDate.parse(cycle[0].date) - const fiveDayEndDate = startDate.plusDays(4).toString() - const fiveDayRuleDays = cycle.slice(0, 5).filter(d => d.date <= fiveDayEndDate) - const preOvulatoryDays = getDaysUntilFertileMucus(fiveDayRuleDays) + const preOvuPhaseEndDate = startDate.plusDays(preOvuPhaseLength - 1).toString() + const maybePreOvuDays = cycle.slice(0, 5).filter(d => d.date <= preOvuPhaseEndDate) + const preOvulatoryDays = getDaysUntilFertileMucus(maybePreOvuDays) let endDate - if (preOvulatoryDays.length === fiveDayRuleDays.length) { - endDate = fiveDayEndDate + if (preOvulatoryDays.length === maybePreOvuDays.length) { + endDate = preOvuPhaseEndDate } else { endDate = preOvulatoryDays[preOvulatoryDays.length - 1].date } @@ -29,4 +36,6 @@ function getDaysUntilFertileMucus(days) { return days.slice(0, firstFertileMucusDayIndex) } return days -} \ No newline at end of file +} + +function apply8DayRule() {} \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index ebc0106..eaf1e5c 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -23,7 +23,7 @@ describe('sympto', () => { it('with no shifts detects only peri-ovulatory', function () { const status = getSensiplanStatus({ cycle: cycleWithoutAnyShifts, - previousCycle: cycleWithoutTempShift + previousCycles: [cycleWithoutTempShift] }) expect(status).to.eql({ @@ -40,7 +40,7 @@ describe('sympto', () => { it('with shifts detects only peri-ovulatory and post-ovulatory', function () { const status = getSensiplanStatus({ cycle: cycleWithTempAndMucusShift, - previousCycle: cycleWithoutTempShift + previousCycles: [cycleWithoutTempShift] }) expect(status.temperatureShift).to.be.an('object') @@ -66,7 +66,7 @@ describe('sympto', () => { it('according to 5-day-rule', function () { const status = getSensiplanStatus({ cycle: fiveDayCycle, - previousCycle: cycleWithTempShift + previousCycles: [cycleWithTempShift] }) expect(Object.keys(status.phases).length).to.eql(1) @@ -83,7 +83,7 @@ describe('sympto', () => { it('according to 5-day-rule', function () { const status = getSensiplanStatus({ cycle: cycleWithTempAndNoMucusShift, - previousCycle: cycleWithTempShift + previousCycles: [cycleWithTempShift] }) expect(Object.keys(status.phases).length).to.eql(2) @@ -101,7 +101,7 @@ describe('sympto', () => { it('according to 5-day-rule with shortened pre-phase', function () { const status = getSensiplanStatus({ cycle: cycleWithEarlyMucus, - previousCycle: cycleWithTempShift + previousCycles: [cycleWithTempShift] }) expect(Object.keys(status.phases).length).to.eql(2) @@ -121,7 +121,7 @@ describe('sympto', () => { it('according to 5-day-rule', function () { const status = getSensiplanStatus({ cycle: cycleWithTempAndMucusShift, - previousCycle: cycleWithTempShift + previousCycles: [cycleWithTempShift] }) expect(Object.keys(status.phases).length).to.eql(3) @@ -149,7 +149,7 @@ describe('sympto', () => { it('with fhM + mucus peak on same day finds correct start of post-ovu phase', () => { const status = getSensiplanStatus({ cycle: mucusPeakAndFhmOnSameDay, - previousCycle: cycleWithTempShift + previousCycles: [cycleWithTempShift] }) expect(status.temperatureShift).to.be.an('object') @@ -180,7 +180,7 @@ describe('sympto', () => { it('with fhM 2 days before mucus peak waits for end of mucus eval', () => { const status = getSensiplanStatus({ cycle: fhmTwoDaysBeforeMucusPeak, - previousCycle: cycleWithTempShift + previousCycles: [cycleWithTempShift] }) expect(status.temperatureShift).to.be.an('object') @@ -211,7 +211,7 @@ describe('sympto', () => { it('with another mucus peak 5 days after fHM ignores it', () => { const status = getSensiplanStatus({ cycle: mucusPeak5DaysAfterFhm, - previousCycle: cycleWithTempShift + previousCycles: [cycleWithTempShift] }) expect(status.temperatureShift).to.be.an('object') @@ -242,7 +242,7 @@ describe('sympto', () => { it('with mucus peak 2 days before fhM waits for end of temp eval', () => { const status = getSensiplanStatus({ cycle: mucusPeakTwoDaysBeforeFhm, - previousCycle: cycleWithTempShift + previousCycles: [cycleWithTempShift] }) expect(status.temperatureShift).to.be.an('object') @@ -273,7 +273,7 @@ describe('sympto', () => { it('with mucus peak 5 days before fhM waits for end of temp eval', () => { const status = getSensiplanStatus({ cycle: fhm5DaysAfterMucusPeak, - previousCycle: cycleWithTempShift + previousCycles: [cycleWithTempShift] }) expect(status.temperatureShift).to.be.an('object') @@ -302,6 +302,15 @@ describe('sympto', () => { }) }) + describe('applying the minus-8 rule', () => { + it('shortens the pre-ovu phase if there is a previous <13 fhm') + it('shortens the pre-ovu phase if there is a previous <13 fhm with less than 12 cycles') + it('shortens the pre-ovu phase if mucus occurs') + it('lengthens the pre-ovu phase if >= 12 cycles') + it('does not lengthen the pre-ovu phase if < 12 cycles') + it('does not lengthen the pre-ovu phase if < 12 cycles') + }) + describe('when args are wrong', () => { it('throws when arg object is not in right format', () => { const wrongObject = { hello: 'world' } @@ -316,10 +325,10 @@ describe('sympto', () => { hello: 'world', bleeding: { value: 0 } }], - previousCycle: [{ + previousCycles: [[{ date: '1992-09-09', bleeding: { value: 0 } - }] + }]] })).to.throw(AssertionError) expect(() => getSensiplanStatus({ cycle: [{ @@ -327,20 +336,20 @@ describe('sympto', () => { temperature: {value: '35'}, bleeding: { value: 0 } }], - previousCycle: [{ + previousCycles: [[{ date: '1992-09-09', bleeding: { value: 0 } - }] + }]] })).to.throw(AssertionError) expect(() => getSensiplanStatus({ cycle: [{ date: '09-14-2017', bleeding: { value: 0 } }], - previousCycle: [{ + previousCycles: [[{ date: '1992-09-09', bleeding: { value: 0 } - }] + }]] })).to.throw(AssertionError) }) it('throws if first cycle day does not have bleeding value', () => { @@ -351,11 +360,11 @@ describe('sympto', () => { value: 'medium' } }], - previousCycle: [ + previousCycles: [[ { date: '2017-09-23', } - ] + ]] })).to.throw(AssertionError) }) }) From 93ff7a0f8f4296205194d864aba171dc42d3f200 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 12 Jul 2018 11:55:42 +0200 Subject: [PATCH 34/59] Set up failing test for minus 8 rule --- lib/sympto/pre-ovulatory.js | 2 +- test/sympto/fixtures.js | 50 ++++++++++++++++++------------------- test/sympto/index.spec.js | 34 +++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/lib/sympto/pre-ovulatory.js b/lib/sympto/pre-ovulatory.js index 5749e1a..7d6e15a 100644 --- a/lib/sympto/pre-ovulatory.js +++ b/lib/sympto/pre-ovulatory.js @@ -38,4 +38,4 @@ function getDaysUntilFertileMucus(days) { return days } -function apply8DayRule() {} \ No newline at end of file +function apply8DayRule(previousCycles) {} \ No newline at end of file diff --git a/test/sympto/fixtures.js b/test/sympto/fixtures.js index 1616ed4..5ac51be 100644 --- a/test/sympto/fixtures.js +++ b/test/sympto/fixtures.js @@ -7,17 +7,17 @@ function convertToSymptoFormat(val) { return sympto } -const cycleWithTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8, 36.8] +export const cycleWithTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8, 36.8] .map(num => ({ date: '2018-06-01', temperature: num })) .map(convertToSymptoFormat) cycleWithTempShift.unshift({date: '2018-05-30', bleeding: { value: 2 }}) -const cycleWithoutTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8] +export const cycleWithoutTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8] .map(num => ({ date: '2018-06-01', temperature: num })) .map(convertToSymptoFormat) cycleWithoutTempShift.unshift({date: '2018-05-30', bleeding: { value: 2 }}) -const cycleWithTempAndMucusShift = [ +export const cycleWithTempAndMucusShift = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65 }, { date: '2018-06-04', temperature: 36.6 }, @@ -40,7 +40,7 @@ const cycleWithTempAndMucusShift = [ { date: '2018-06-27', temperature: 36.9, mucus: 1 } ].map(convertToSymptoFormat) -const cycleWithTempAndNoMucusShift = [ +export const cycleWithTempAndNoMucusShift = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65 }, { date: '2018-06-05', temperature: 36.55 }, @@ -63,7 +63,7 @@ const cycleWithTempAndNoMucusShift = [ { date: '2018-06-27', temperature: 36.9, mucus: 4 } ].map(convertToSymptoFormat) -const cycleWithEarlyMucus = [ +export const cycleWithEarlyMucus = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65, mucus: 3 }, { date: '2018-06-05', temperature: 36.55 }, @@ -86,7 +86,7 @@ const cycleWithEarlyMucus = [ { date: '2018-06-27', temperature: 36.9, mucus: 4 } ].map(convertToSymptoFormat) -const cycleWithoutAnyShifts = [ +export const cycleWithoutAnyShifts = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65 }, { date: '2018-06-04', temperature: 36.6 }, @@ -96,12 +96,12 @@ const cycleWithoutAnyShifts = [ { date: '2018-06-08', temperature: 36.45, mucus: 1 } ].map(convertToSymptoFormat) -const fiveDayCycle = [ +export const fiveDayCycle = [ { date: '2018-06-01', bleeding: 2 }, { date: '2018-06-03', bleeding: 3 }, ].map(convertToSymptoFormat) -const mucusPeakAndFhmOnSameDay = [ +export const mucusPeakAndFhmOnSameDay = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65 }, { date: '2018-06-04', temperature: 36.6 }, @@ -124,7 +124,7 @@ const mucusPeakAndFhmOnSameDay = [ { date: '2018-06-27', temperature: 36.9, mucus: 1 } ].map(convertToSymptoFormat) -const fhmTwoDaysBeforeMucusPeak = [ +export const fhmTwoDaysBeforeMucusPeak = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65 }, { date: '2018-06-04', temperature: 36.6 }, @@ -146,7 +146,7 @@ const fhmTwoDaysBeforeMucusPeak = [ { date: '2018-06-26', temperature: 36.8, mucus: 1 }, ].map(convertToSymptoFormat) -const mucusPeakTwoDaysBeforeFhm = [ +export const mucusPeakTwoDaysBeforeFhm = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65 }, { date: '2018-06-04', temperature: 36.6 }, @@ -165,7 +165,7 @@ const mucusPeakTwoDaysBeforeFhm = [ { date: '2018-07-04', temperature: 36.8, mucus: 2 }, ].map(convertToSymptoFormat) -const mucusPeak5DaysAfterFhm = [ +export const mucusPeak5DaysAfterFhm = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65, mucus: 2 }, { date: '2018-06-04', temperature: 36.6 }, @@ -190,7 +190,7 @@ const mucusPeak5DaysAfterFhm = [ { date: '2018-07-02', temperature: 36.9, mucus: 1 } ].map(convertToSymptoFormat) -const fhm5DaysAfterMucusPeak = [ +export const fhm5DaysAfterMucusPeak = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65 }, { date: '2018-06-04', temperature: 36.6 }, @@ -213,17 +213,15 @@ const fhm5DaysAfterMucusPeak = [ { date: '2018-06-27', temperature: 36.9, mucus: 1 } ].map(convertToSymptoFormat) -export { - cycleWithoutTempShift, - cycleWithTempAndMucusShift, - cycleWithTempAndNoMucusShift, - cycleWithTempShift, - cycleWithoutAnyShifts, - fiveDayCycle, - cycleWithEarlyMucus, - fhm5DaysAfterMucusPeak, - fhmTwoDaysBeforeMucusPeak, - mucusPeak5DaysAfterFhm, - mucusPeakAndFhmOnSameDay, - mucusPeakTwoDaysBeforeFhm -} \ No newline at end of file +export const fhmOnDay12 = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-09', temperature: 36.5, mucus: 2 }, + { date: '2018-06-10', temperature: 36.4, mucus: 3 }, + { date: '2018-06-12', temperature: 36.8, mucus: 3 }, + { date: '2018-06-14', temperature: 36.9, mucus: 2 }, + { date: '2018-06-17', temperature: 36.9, mucus: 2 }, +].map(convertToSymptoFormat) diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index eaf1e5c..4b00183 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -13,7 +13,8 @@ import { fhmTwoDaysBeforeMucusPeak, fhm5DaysAfterMucusPeak, mucusPeak5DaysAfterFhm, - mucusPeakTwoDaysBeforeFhm + mucusPeakTwoDaysBeforeFhm, + fhmOnDay12 } from './fixtures' const expect = chai.expect @@ -303,7 +304,36 @@ describe('sympto', () => { }) describe('applying the minus-8 rule', () => { - it('shortens the pre-ovu phase if there is a previous <13 fhm') + it('shortens the pre-ovu phase if there is a previous <13 fhm', () => { + const status = getSensiplanStatus({ + cycle: cycleWithTempAndMucusShift, + previousCycles: [fhmOnDay12, cycleWithTempShift] + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-04' }, + cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => date <= '2018-06-04') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-05' }, + end: { date: '2018-06-17', time: '18:00' }, + cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => { + return date > '2018-06-04' && date <= '2018-06-17' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-17', + time: '18:00' + }, + cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => date >= '2018-06-17') + }) + }) it('shortens the pre-ovu phase if there is a previous <13 fhm with less than 12 cycles') it('shortens the pre-ovu phase if mucus occurs') it('lengthens the pre-ovu phase if >= 12 cycles') From d5aa903da402798d7285e4ab47b3df01c9c903c6 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Fri, 13 Jul 2018 06:44:23 +0200 Subject: [PATCH 35/59] Check for fhm according to nfp, not just temp shift --- lib/sympto/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 36f8e5e..2005f32 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -4,19 +4,18 @@ import getPreOvulatoryPhase from './pre-ovulatory' import { LocalDate } from 'js-joda' import assert from 'assert' -export default function ({ cycle, previousCycles = [] }) { +export default function getSymptoThermalStatus({ cycle, previousCycles = [] }) { throwIfArgsAreNotInRequiredFormat(cycle, previousCycles) const status = { assumeFertility: true, phases: {} } - // TODO handle no previous cycles // if there was no first higher measurement in the previous cycle, // no infertile pre-ovulatory phase may be assumed const lastCycle = previousCycles[previousCycles.length - 1] - if (getTemperatureShift(lastCycle).detected && !cycle[0].mucus) { + if (lastCycle && getSymptoThermalStatus({cycle: lastCycle}).temperatureShift) { status.phases.preOvulatory = getPreOvulatoryPhase(cycle, previousCycles) if (status.phases.preOvulatory.cycleDays.length === cycle.length) { status.assumeFertility = false From 7382bff0dddeddecb781a8b23439b41ae61bcb54 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Fri, 13 Jul 2018 07:14:36 +0200 Subject: [PATCH 36/59] Implement minus 8 day rule --- .eslintrc | 3 +- lib/sympto/minus-8-day-rule.js | 26 +++ lib/sympto/pre-ovulatory.js | 20 ++- test/sympto/fixtures.js | 47 +++++- test/sympto/index.spec.js | 297 +++++++++++++++++++++++++-------- 5 files changed, 308 insertions(+), 85 deletions(-) create mode 100644 lib/sympto/minus-8-day-rule.js diff --git a/.eslintrc b/.eslintrc index 24f9ab5..40d7e9b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -47,6 +47,7 @@ "no-var": "error", "prefer-const": "error", "no-trailing-spaces": "error", - "react/prop-types": 0 + "react/prop-types": 0, + "max-len": "warn" } } \ No newline at end of file diff --git a/lib/sympto/minus-8-day-rule.js b/lib/sympto/minus-8-day-rule.js new file mode 100644 index 0000000..2964b37 --- /dev/null +++ b/lib/sympto/minus-8-day-rule.js @@ -0,0 +1,26 @@ +import { LocalDate } from 'js-joda' +import getNfpStatus from './index' + +export default function (previousCycles) { + const fhms = previousCycles + .map(cycle => { + const status = getNfpStatus({ cycle }) + if (status.temperatureShift) { + const day = status.temperatureShift.firstHighMeasurementDay + const firstCycleDayDate = LocalDate.parse(cycle[0].date) + const fhmDate = LocalDate.parse(day.date) + return fhmDate.compareTo(firstCycleDayDate) + 1 + } + return null + }) + .filter(val => typeof val === 'number') + + const preOvuLength = Math.min(...fhms) - 8 + + // pre ovu length may only be lengthened if we have more than 12 previous fhms + // if pre ovu length is less than 5, it shortened even with fewer prev fhms + if (preOvuLength < 5) return preOvuLength + if (fhms.length >= 12) return preOvuLength + + return null +} \ No newline at end of file diff --git a/lib/sympto/pre-ovulatory.js b/lib/sympto/pre-ovulatory.js index 7d6e15a..01efb19 100644 --- a/lib/sympto/pre-ovulatory.js +++ b/lib/sympto/pre-ovulatory.js @@ -1,20 +1,21 @@ import { LocalDate } from "js-joda" +import apply8DayRule from './minus-8-day-rule' export default function(cycle, previousCycles) { - // TODO handle no previous cycles let preOvuPhaseLength = 5 - //TODO make sure it handles weird cases like fhm < 9 const minus8DayRuleResult = apply8DayRule(previousCycles) if (minus8DayRuleResult) preOvuPhaseLength = minus8DayRuleResult const startDate = LocalDate.parse(cycle[0].date) - const preOvuPhaseEndDate = startDate.plusDays(preOvuPhaseLength - 1).toString() - const maybePreOvuDays = cycle.slice(0, 5).filter(d => d.date <= preOvuPhaseEndDate) + const preOvuEndDate = startDate.plusDays(preOvuPhaseLength - 1).toString() + const maybePreOvuDays = cycle.slice(0, preOvuPhaseLength).filter(d => { + return d.date <= preOvuEndDate + }) const preOvulatoryDays = getDaysUntilFertileMucus(maybePreOvuDays) let endDate if (preOvulatoryDays.length === maybePreOvuDays.length) { - endDate = preOvuPhaseEndDate + endDate = preOvuEndDate } else { endDate = preOvulatoryDays[preOvulatoryDays.length - 1].date } @@ -31,11 +32,12 @@ export default function(cycle, previousCycles) { } function getDaysUntilFertileMucus(days) { - const firstFertileMucusDayIndex = days.findIndex(day => day.mucus && day.mucus.value > 1) + const firstFertileMucusDayIndex = days.findIndex(day => { + return day.mucus && day.mucus.value > 1 + }) + if (firstFertileMucusDayIndex > -1) { return days.slice(0, firstFertileMucusDayIndex) } return days -} - -function apply8DayRule(previousCycles) {} \ No newline at end of file +} \ No newline at end of file diff --git a/test/sympto/fixtures.js b/test/sympto/fixtures.js index 5ac51be..ce096fa 100644 --- a/test/sympto/fixtures.js +++ b/test/sympto/fixtures.js @@ -7,17 +7,31 @@ function convertToSymptoFormat(val) { return sympto } -export const cycleWithTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8, 36.8] - .map(num => ({ date: '2018-06-01', temperature: num })) - .map(convertToSymptoFormat) -cycleWithTempShift.unshift({date: '2018-05-30', bleeding: { value: 2 }}) +export const cycleWithFhm = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-13', temperature: 36.8, mucus: 4 }, + { date: '2018-06-15', temperature: 36.9, mucus: 2 }, + { date: '2018-06-17', temperature: 36.9, mucus: 2 }, + { date: '2018-06-17', temperature: 36.9, mucus: 2 }, + { date: '2018-06-18', temperature: 36.9, mucus: 2 } +].map(convertToSymptoFormat) -export const cycleWithoutTempShift = [36.6, 36.6, 36.6, 36.6, 36.6, 36.6, 36.8, 36.8] - .map(num => ({ date: '2018-06-01', temperature: num })) - .map(convertToSymptoFormat) -cycleWithoutTempShift.unshift({date: '2018-05-30', bleeding: { value: 2 }}) +export const cycleWithoutFhm = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-09', temperature: 36.8, mucus: 4 }, + { date: '2018-06-10', temperature: 36.9, mucus: 2 }, + { date: '2018-06-13', temperature: 36.9, mucus: 2 } +].map(convertToSymptoFormat) -export const cycleWithTempAndMucusShift = [ +export const longAndComplicatedCycle = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65 }, { date: '2018-06-04', temperature: 36.6 }, @@ -224,4 +238,19 @@ export const fhmOnDay12 = [ { date: '2018-06-12', temperature: 36.8, mucus: 3 }, { date: '2018-06-14', temperature: 36.9, mucus: 2 }, { date: '2018-06-17', temperature: 36.9, mucus: 2 }, + { date: '2018-06-18', temperature: 36.9, mucus: 2 }, ].map(convertToSymptoFormat) + +export const fhmOnDay15 = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-09', temperature: 36.5, mucus: 2 }, + { date: '2018-06-10', temperature: 36.4, mucus: 3 }, + { date: '2018-06-15', temperature: 36.8, mucus: 3 }, + { date: '2018-06-16', temperature: 36.9, mucus: 2 }, + { date: '2018-06-17', temperature: 36.9, mucus: 2 }, + { date: '2018-06-18', temperature: 36.9, mucus: 2 }, +].map(convertToSymptoFormat) \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index 4b00183..bbb66d4 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -2,10 +2,10 @@ import chai from 'chai' import getSensiplanStatus from '../../lib/sympto' import { AssertionError } from 'assert' import { - cycleWithoutTempShift, - cycleWithTempAndMucusShift, + cycleWithoutFhm, + longAndComplicatedCycle, cycleWithTempAndNoMucusShift, - cycleWithTempShift, + cycleWithFhm, cycleWithoutAnyShifts, fiveDayCycle, cycleWithEarlyMucus, @@ -14,7 +14,8 @@ import { fhm5DaysAfterMucusPeak, mucusPeak5DaysAfterFhm, mucusPeakTwoDaysBeforeFhm, - fhmOnDay12 + fhmOnDay12, + fhmOnDay15 } from './fixtures' const expect = chai.expect @@ -24,7 +25,7 @@ describe('sympto', () => { it('with no shifts detects only peri-ovulatory', function () { const status = getSensiplanStatus({ cycle: cycleWithoutAnyShifts, - previousCycles: [cycleWithoutTempShift] + previousCycles: [cycleWithoutFhm,] }) expect(status).to.eql({ @@ -38,10 +39,10 @@ describe('sympto', () => { }) }) - it('with shifts detects only peri-ovulatory and post-ovulatory', function () { + it('with shifts detects only peri-ovulatory and post-ovulatory', () => { const status = getSensiplanStatus({ - cycle: cycleWithTempAndMucusShift, - previousCycles: [cycleWithoutTempShift] + cycle: longAndComplicatedCycle, + previousCycles: [cycleWithoutFhm,] }) expect(status.temperatureShift).to.be.an('object') @@ -51,14 +52,16 @@ describe('sympto', () => { expect(status.phases.periOvulatory).to.eql({ start: { date: '2018-06-01' }, end: { date: '2018-06-21', time: '18:00' }, - cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date <= '2018-06-21') + cycleDays: longAndComplicatedCycle + .filter(({date}) => date <= '2018-06-21') }) expect(status.phases.postOvulatory).to.eql({ start: { date: '2018-06-21', time: '18:00' }, - cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date >= '2018-06-21') + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-21') }) }) }) @@ -67,7 +70,7 @@ describe('sympto', () => { it('according to 5-day-rule', function () { const status = getSensiplanStatus({ cycle: fiveDayCycle, - previousCycles: [cycleWithTempShift] + previousCycles: [cycleWithFhm] }) expect(Object.keys(status.phases).length).to.eql(1) @@ -80,29 +83,31 @@ describe('sympto', () => { }) }) - describe('with no shifts detects pre- and peri-ovulatory phase', function () { + describe('with no shifts detects pre- and peri-ovulatory phase', () => { it('according to 5-day-rule', function () { const status = getSensiplanStatus({ cycle: cycleWithTempAndNoMucusShift, - previousCycles: [cycleWithTempShift] + previousCycles: [cycleWithFhm] }) expect(Object.keys(status.phases).length).to.eql(2) expect(status.assumeFertility).to.be.true() expect(status.phases.preOvulatory).to.eql({ - cycleDays: cycleWithTempAndNoMucusShift.filter(({date}) => date <= '2018-06-05'), + cycleDays: cycleWithTempAndNoMucusShift + .filter(({date}) => date <= '2018-06-05'), start: { date: '2018-06-01' }, end: { date: '2018-06-05' } }) expect(status.phases.periOvulatory).to.eql({ - cycleDays: cycleWithTempAndNoMucusShift.filter(({date}) => date > '2018-06-05'), + cycleDays: cycleWithTempAndNoMucusShift + .filter(({date}) => date > '2018-06-05'), start: { date: '2018-06-06' } }) }) it('according to 5-day-rule with shortened pre-phase', function () { const status = getSensiplanStatus({ cycle: cycleWithEarlyMucus, - previousCycles: [cycleWithTempShift] + previousCycles: [cycleWithFhm] }) expect(Object.keys(status.phases).length).to.eql(2) @@ -121,24 +126,27 @@ describe('sympto', () => { describe('with shifts detects pre- and peri-ovulatory phase', function () { it('according to 5-day-rule', function () { const status = getSensiplanStatus({ - cycle: cycleWithTempAndMucusShift, - previousCycles: [cycleWithTempShift] + cycle: longAndComplicatedCycle, + previousCycles: [cycleWithFhm] }) expect(Object.keys(status.phases).length).to.eql(3) expect(status.assumeFertility).to.be.false() expect(status.phases.preOvulatory).to.eql({ - cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date <= '2018-06-05'), + cycleDays: longAndComplicatedCycle + .filter(({date}) => date <= '2018-06-05'), start: { date: '2018-06-01' }, end: { date: '2018-06-05' } }) expect(status.phases.periOvulatory).to.eql({ - cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date > '2018-06-05' && date <= '2018-06-21'), + cycleDays: longAndComplicatedCycle + .filter(({date}) => date > '2018-06-05' && date <= '2018-06-21'), start: { date: '2018-06-06' }, end: { date: '2018-06-21', time: '18:00'} }) expect(status.phases.postOvulatory).to.eql({ - cycleDays: cycleWithTempAndMucusShift.filter(({date}) => date >= '2018-06-21'), + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-21'), start: { date: '2018-06-21', time: '18:00'} }) }) @@ -147,10 +155,10 @@ describe('sympto', () => { }) describe('combining first higher measurment and mucus peak', () => { - it('with fhM + mucus peak on same day finds correct start of post-ovu phase', () => { + it('with fhM + mucus peak on same day finds start of postovu phase', () => { const status = getSensiplanStatus({ cycle: mucusPeakAndFhmOnSameDay, - previousCycles: [cycleWithTempShift] + previousCycles: [cycleWithFhm] }) expect(status.temperatureShift).to.be.an('object') @@ -160,28 +168,31 @@ describe('sympto', () => { expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, end: { date: '2018-06-05' }, - cycleDays: mucusPeakAndFhmOnSameDay.filter(({date}) => date <= '2018-06-05') + cycleDays: mucusPeakAndFhmOnSameDay + .filter(({date}) => date <= '2018-06-05') }) expect(status.phases.periOvulatory).to.eql({ start: { date: '2018-06-06' }, end: { date: '2018-06-21', time: '18:00' }, - cycleDays: mucusPeakAndFhmOnSameDay.filter(({date}) => { - return date > '2018-06-05' && date <= '2018-06-21' - }) + cycleDays: mucusPeakAndFhmOnSameDay + .filter(({date}) => { + return date > '2018-06-05' && date <= '2018-06-21' + }) }) expect(status.phases.postOvulatory).to.eql({ start: { date: '2018-06-21', time: '18:00' }, - cycleDays: mucusPeakAndFhmOnSameDay.filter(({date}) => date >= '2018-06-21') + cycleDays: mucusPeakAndFhmOnSameDay + .filter(({date}) => date >= '2018-06-21') }) }) it('with fhM 2 days before mucus peak waits for end of mucus eval', () => { const status = getSensiplanStatus({ cycle: fhmTwoDaysBeforeMucusPeak, - previousCycles: [cycleWithTempShift] + previousCycles: [cycleWithFhm] }) expect(status.temperatureShift).to.be.an('object') @@ -191,28 +202,31 @@ describe('sympto', () => { expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, end: { date: '2018-06-05' }, - cycleDays: fhmTwoDaysBeforeMucusPeak.filter(({date}) => date <= '2018-06-05') + cycleDays: fhmTwoDaysBeforeMucusPeak + .filter(({date}) => date <= '2018-06-05') }) expect(status.phases.periOvulatory).to.eql({ start: { date: '2018-06-06' }, end: { date: '2018-06-26', time: '18:00' }, - cycleDays: fhmTwoDaysBeforeMucusPeak.filter(({date}) => { - return date > '2018-06-05' && date <= '2018-06-26' - }) + cycleDays: fhmTwoDaysBeforeMucusPeak + .filter(({date}) => { + return date > '2018-06-05' && date <= '2018-06-26' + }) }) expect(status.phases.postOvulatory).to.eql({ start: { date: '2018-06-26', time: '18:00' }, - cycleDays: fhmTwoDaysBeforeMucusPeak.filter(({date}) => date >= '2018-06-26') + cycleDays: fhmTwoDaysBeforeMucusPeak + .filter(({date}) => date >= '2018-06-26') }) }) it('with another mucus peak 5 days after fHM ignores it', () => { const status = getSensiplanStatus({ cycle: mucusPeak5DaysAfterFhm, - previousCycles: [cycleWithTempShift] + previousCycles: [cycleWithFhm] }) expect(status.temperatureShift).to.be.an('object') @@ -222,28 +236,31 @@ describe('sympto', () => { expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, end: { date: '2018-06-01' }, - cycleDays: mucusPeak5DaysAfterFhm.filter(({date}) => date <= '2018-06-01') + cycleDays: mucusPeak5DaysAfterFhm + .filter(({date}) => date <= '2018-06-01') }) expect(status.phases.periOvulatory).to.eql({ start: { date: '2018-06-02' }, end: { date: '2018-06-22', time: '18:00' }, - cycleDays: mucusPeak5DaysAfterFhm.filter(({date}) => { - return date > '2018-06-01' && date <= '2018-06-22' - }) + cycleDays: mucusPeak5DaysAfterFhm + .filter(({date}) => { + return date > '2018-06-01' && date <= '2018-06-22' + }) }) expect(status.phases.postOvulatory).to.eql({ start: { date: '2018-06-22', time: '18:00' }, - cycleDays: mucusPeak5DaysAfterFhm.filter(({date}) => date >= '2018-06-22') + cycleDays: mucusPeak5DaysAfterFhm + .filter(({date}) => date >= '2018-06-22') }) }) it('with mucus peak 2 days before fhM waits for end of temp eval', () => { const status = getSensiplanStatus({ cycle: mucusPeakTwoDaysBeforeFhm, - previousCycles: [cycleWithTempShift] + previousCycles: [cycleWithFhm] }) expect(status.temperatureShift).to.be.an('object') @@ -253,28 +270,31 @@ describe('sympto', () => { expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, end: { date: '2018-06-04' }, - cycleDays: mucusPeakTwoDaysBeforeFhm.filter(({date}) => date <= '2018-06-04') + cycleDays: mucusPeakTwoDaysBeforeFhm + .filter(({date}) => date <= '2018-06-04') }) expect(status.phases.periOvulatory).to.eql({ start: { date: '2018-06-05' }, end: { date: '2018-07-03', time: '18:00' }, - cycleDays: mucusPeakTwoDaysBeforeFhm.filter(({date}) => { - return date > '2018-06-04' && date <= '2018-07-03' - }) + cycleDays: mucusPeakTwoDaysBeforeFhm + .filter(({date}) => { + return date > '2018-06-04' && date <= '2018-07-03' + }) }) expect(status.phases.postOvulatory).to.eql({ start: { date: '2018-07-03', time: '18:00' }, - cycleDays: mucusPeakTwoDaysBeforeFhm.filter(({date}) => date >= '2018-07-03') + cycleDays: mucusPeakTwoDaysBeforeFhm + .filter(({date}) => date >= '2018-07-03') }) }) it('with mucus peak 5 days before fhM waits for end of temp eval', () => { const status = getSensiplanStatus({ cycle: fhm5DaysAfterMucusPeak, - previousCycles: [cycleWithTempShift] + previousCycles: [cycleWithFhm] }) expect(status.temperatureShift).to.be.an('object') @@ -284,21 +304,24 @@ describe('sympto', () => { expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, end: { date: '2018-06-05' }, - cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => date <= '2018-06-05') + cycleDays: fhm5DaysAfterMucusPeak + .filter(({date}) => date <= '2018-06-05') }) expect(status.phases.periOvulatory).to.eql({ start: { date: '2018-06-06' }, end: { date: '2018-06-21', time: '18:00' }, - cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => { - return date > '2018-06-05' && date <= '2018-06-21' - }) + cycleDays: fhm5DaysAfterMucusPeak + .filter(({date}) => { + return date > '2018-06-05' && date <= '2018-06-21' + }) }) expect(status.phases.postOvulatory).to.eql({ start: { date: '2018-06-21', time: '18:00' }, - cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => date >= '2018-06-21') + cycleDays: fhm5DaysAfterMucusPeak + .filter(({date}) => date >= '2018-06-21') }) }) }) @@ -306,8 +329,8 @@ describe('sympto', () => { describe('applying the minus-8 rule', () => { it('shortens the pre-ovu phase if there is a previous <13 fhm', () => { const status = getSensiplanStatus({ - cycle: cycleWithTempAndMucusShift, - previousCycles: [fhmOnDay12, cycleWithTempShift] + cycle: longAndComplicatedCycle, + previousCycles: [fhmOnDay12, ...Array(13).fill(fhmOnDay15)] }) expect(status.temperatureShift).to.be.an('object') @@ -317,28 +340,170 @@ describe('sympto', () => { expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, end: { date: '2018-06-04' }, - cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => date <= '2018-06-04') + cycleDays: longAndComplicatedCycle + .filter(({date}) => date <= '2018-06-04') }) expect(status.phases.periOvulatory).to.eql({ start: { date: '2018-06-05' }, - end: { date: '2018-06-17', time: '18:00' }, - cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => { - return date > '2018-06-04' && date <= '2018-06-17' - }) + end: { date: '2018-06-21', time: '18:00' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => { + return date > '2018-06-04' && date <= '2018-06-21' + }) }) expect(status.phases.postOvulatory).to.eql({ start: { - date: '2018-06-17', + date: '2018-06-21', time: '18:00' }, - cycleDays: fhm5DaysAfterMucusPeak.filter(({date}) => date >= '2018-06-17') + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-21') + }) + }) + it('shortens pre-ovu phase with prev <13 fhm even with <12 cycles', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycles: Array(11).fill(fhmOnDay12) + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-04' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date <= '2018-06-04') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-05' }, + end: { date: '2018-06-21', time: '18:00' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => { + return date > '2018-06-04' && date <= '2018-06-21' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-21', + time: '18:00' + }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-21') + }) + }) + it('shortens the pre-ovu phase if mucus occurs', () => { + const status = getSensiplanStatus({ + cycle: cycleWithEarlyMucus, + previousCycles: Array(11).fill(fhmOnDay12) + }) + + expect(status.assumeFertility).to.be.true() + expect(Object.keys(status.phases).length).to.eql(2) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-01' }, + cycleDays: cycleWithEarlyMucus + .filter(({date}) => date <= '2018-06-01') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-02' }, + cycleDays: cycleWithEarlyMucus + .filter(({date}) => { + return date > '2018-06-01' + }) + }) + }) + it('lengthens the pre-ovu phase if >= 12 cycles with fhm > 13', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycles: Array(12).fill(fhmOnDay15) + }) + + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-07' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date <= '2018-06-07') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-08' }, + end: { date: '2018-06-21', time: '18:00' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => { + return date > '2018-06-07' && date <= '2018-06-21' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-21', + time: '18:00' + }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-21') + }) + }) + + it('does not lengthen the pre-ovu phase if < 12 cycles', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycles: Array(11).fill(fhmOnDay15) + }) + + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-05' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date <= '2018-06-05') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-06' }, + end: { date: '2018-06-21', time: '18:00' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => { + return date > '2018-06-05' && date <= '2018-06-21' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-21', + time: '18:00' + }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-21') + }) + }) + + it('does not detect any pre-ovu phase if prev cycle had no fhm', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycles: [...Array(12).fill(fhmOnDay15), cycleWithoutFhm] + }) + + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(2) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-21', time: '18:00' }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => { + return date >= '2018-06-01' && date <= '2018-06-21' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-21', + time: '18:00' + }, + cycleDays: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-21') }) }) - it('shortens the pre-ovu phase if there is a previous <13 fhm with less than 12 cycles') - it('shortens the pre-ovu phase if mucus occurs') - it('lengthens the pre-ovu phase if >= 12 cycles') - it('does not lengthen the pre-ovu phase if < 12 cycles') - it('does not lengthen the pre-ovu phase if < 12 cycles') }) describe('when args are wrong', () => { From b3467005e15832741999d1b2ea010c5e31237c58 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Fri, 13 Jul 2018 07:20:52 +0200 Subject: [PATCH 37/59] Fix line length linter warnings --- test/sympto/temperature.spec.js | 83 +++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/test/sympto/temperature.spec.js b/test/sympto/temperature.spec.js index d5131b8..ce7e6c1 100644 --- a/test/sympto/temperature.spec.js +++ b/test/sympto/temperature.spec.js @@ -13,15 +13,16 @@ function turnIntoCycleDayObject(value, fakeDate) { describe('sympto', () => { describe('detect temperature shift', () => { describe('regular rule', () => { - it('reports lower temperature status before shift', function () { + it('reports lower temperature status before shift', () => { const lowerTemps = [36.7, 36.57, 36.47, 36.49, 36.57] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(lowerTemps) expect(status).to.eql({ detected: false }) }) - it('detects temperature shift correctly', function () { - const tempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] + it('detects temperature shift correctly', () => { + const tempShift = + [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(tempShift) expect(status).to.eql({ @@ -39,29 +40,33 @@ describe('sympto', () => { }) }) - it('detects no temperature shift when there are no 6 low temps', function () { + it('detects no temperature shift when there are no 6 low temps', () => { const tempShift = [36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(tempShift) expect(status).to.eql({ detected: false }) }) - it('detects no temperature shift if the shift is not high enough', function () { - const tempShift = [36.57, 36.7, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] + it('detects no temperature shift if the shift is not high enough', () => { + const tempShift = + [36.57, 36.7, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.8] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(tempShift) expect(status).to.eql({ detected: false }) }) - it('detects missing temperature shift correctly', function () { - const noTempShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] + it('detects missing temperature shift correctly', () => { + const noTempShift = + [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(noTempShift) expect(status).to.eql({ detected: false }) }) - it('detects shift after an earlier one was invalid', function () { - const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.7, 36.8, 36.9] + it('detects shift after an earlier one was invalid', () => { + const temps = + [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, + 36.7, 36.8, 36.9] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) @@ -80,8 +85,10 @@ describe('sympto', () => { }) }) - it('detects 2 consecutive invalid shifts', function () { - const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.6, 36.6, 36.7] + it('detects 2 consecutive invalid shifts', () => { + const temps = + [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, + 36.6, 36.6, 36.7] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) @@ -90,8 +97,10 @@ describe('sympto', () => { }) describe('1st exception rule', () => { - it('detects temperature shift', function () { - const firstException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77, 36.63] + it('detects temperature shift', () => { + const firstException = + [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, + 36.8, 36.86, 36.77, 36.63] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(firstException) expect(status).to.eql({ @@ -110,23 +119,28 @@ describe('sympto', () => { }) }) - it('detects missing temperature shift correctly', function () { - const firstExceptionNoShift = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77, 36.57] + it('detects missing temperature shift correctly', () => { + const firstExceptionNoShift = + [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, + 36.8, 36.86, 36.77, 36.57] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(firstExceptionNoShift) expect(status).to.eql({ detected: false }) }) - it('detects missing temperature shift with not enough high temps', function () { - const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] + it('detects missing temperature shift with not enough high temps', () => { + const temps = + [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.77] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) expect(status).to.eql({ detected: false }) }) - it('detects shift after an earlier one was invalid', function () { - const temps = [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, 36.7, 36.7, 36.7, 36.7] + it('detects shift after an earlier one was invalid', () => { + const temps = + [36.4, 36.4, 36.4, 36.4, 36.4, 36.4, 36.6, 36.6, 36.4, 36.4, + 36.7, 36.7, 36.7, 36.7] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) @@ -149,8 +163,10 @@ describe('sympto', () => { }) describe('2nd exception rule', () => { - it('detects temperature shift with exception temp eql ltl', function () { - const secondException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.6, 36.8] + it('detects temperature shift with exception temp eql ltl', () => { + const secondException = + [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, + 36.8, 36.86, 36.6, 36.8] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(secondException) expect(status).to.eql({ @@ -169,8 +185,10 @@ describe('sympto', () => { }) }) - it('detects temperature shift with exception temp lower than ltl', function () { - const secondException = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.8] + it('detects temperature shift with exception temp lower than ltl', () => { + const secondException = + [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, + 36.8, 36.86, 36.4, 36.8] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(secondException) expect(status).to.eql({ @@ -190,22 +208,27 @@ describe('sympto', () => { }) - it('detects missing temperature shift correctly', function () { - const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.77, 36.77] + it('detects missing temperature shift correctly', () => { + const temps = + [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, + 36.8, 36.86, 36.4, 36.77, 36.77] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) expect(status).to.eql({ detected: false }) }) - it('detects missing temperature shift when not enough high temps', function () { - const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4] + it('detects missing temperature shift when not enough high temps', () => { + const temps = + [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) expect(status).to.eql({ detected: false }) }) - it('detects shift after an earlier one was invalid', function () { - const temps = [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, 36.77, 36.9, 36.9, 36.86, 37.04] + it('detects shift after an earlier one was invalid', () => { + const temps = + [36.7, 36.57, 36.47, 36.49, 36.57, 36.62, 36.55, 36.8, 36.86, 36.4, + 36.77, 36.9, 36.9, 36.86, 37.04] .map(turnIntoCycleDayObject) const status = getTemperatureStatus(temps) expect(status).to.eql({ From f4aca1f41273aaa47b93286010659e5e9888a7b6 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Fri, 13 Jul 2018 09:07:38 +0200 Subject: [PATCH 38/59] Use RN-compatible assert --- package-lock.json | 51 ++++++++++++++++++++++++++++++++++++++++------- package.json | 1 + 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e923bf..c93bbd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1149,6 +1149,14 @@ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "requires": { + "util": "0.10.3" + } + }, "assert-plus": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", @@ -3474,11 +3482,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3491,15 +3501,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3602,7 +3615,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3612,6 +3626,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3624,17 +3639,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3651,6 +3669,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3723,7 +3742,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3733,6 +3753,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3838,6 +3859,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8120,6 +8142,21 @@ } } }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + } + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 9ce4bc4..159a9f2 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "lint": "eslint app test" }, "dependencies": { + "assert": "^1.4.1", "date-range": "0.0.2", "js-joda": "^1.8.2", "moment": "^2.22.1", From 125fd84d59227e01fe00765131f94fdacfd498dc Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Fri, 13 Jul 2018 09:09:12 +0200 Subject: [PATCH 39/59] Add another test --- test/sympto/fixtures.js | 22 ++++++++++++++++++++++ test/sympto/index.spec.js | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/test/sympto/fixtures.js b/test/sympto/fixtures.js index ce096fa..69792d5 100644 --- a/test/sympto/fixtures.js +++ b/test/sympto/fixtures.js @@ -253,4 +253,26 @@ export const fhmOnDay15 = [ { date: '2018-06-16', temperature: 36.9, mucus: 2 }, { date: '2018-06-17', temperature: 36.9, mucus: 2 }, { date: '2018-06-18', temperature: 36.9, mucus: 2 }, +].map(convertToSymptoFormat) + +export const mucusPeakSlightlyBeforeTempShift = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-07', temperature: 36.4, mucus: 1 }, + { date: '2018-06-08', temperature: 36.35, mucus: 2}, + { date: '2018-06-09', temperature: 36.4, mucus: 2}, + { date: '2018-06-10', temperature: 36.45, mucus: 2}, + { date: '2018-06-11', temperature: 36.4, mucus: 3}, + { date: '2018-06-12', temperature: 36.45, mucus: 3}, + { date: '2018-06-13', temperature: 36.45, mucus: 4}, + { date: '2018-06-14', temperature: 36.55, mucus: 3}, + { date: '2018-06-15', temperature: 36.6, mucus: 3}, + { date: '2018-06-16', temperature: 36.6, mucus: 3}, + { date: '2018-06-17', temperature: 36.55, mucus: 2}, + { date: '2018-06-18', temperature: 36.6, mucus: 1}, + { date: '2018-06-19', temperature: 36.7, mucus: 1}, + { date: '2018-06-20', temperature: 36.75, mucus: 1}, + { date: '2018-06-21', temperature: 36.8, mucus: 1}, + { date: '2018-06-22', temperature: 36.8, mucus: 1} ].map(convertToSymptoFormat) \ No newline at end of file diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index bbb66d4..b9dcd9f 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -15,7 +15,8 @@ import { mucusPeak5DaysAfterFhm, mucusPeakTwoDaysBeforeFhm, fhmOnDay12, - fhmOnDay15 + fhmOnDay15, + mucusPeakSlightlyBeforeTempShift } from './fixtures' const expect = chai.expect @@ -223,6 +224,40 @@ describe('sympto', () => { }) }) + it('another example for mucus peak before temp shift', () => { + const status = getSensiplanStatus({ + cycle: mucusPeakSlightlyBeforeTempShift, + previousCycles: [cycleWithFhm] + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) + expect(status.phases.preOvulatory).to.eql({ + start: { date: '2018-06-01' }, + end: { date: '2018-06-05' }, + cycleDays: mucusPeakSlightlyBeforeTempShift + .filter(({date}) => date <= '2018-06-05') + }) + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-06' }, + end: { date: '2018-06-17', time: '18:00' }, + cycleDays: mucusPeakSlightlyBeforeTempShift + .filter(({date}) => { + return date > '2018-06-05' && date <= '2018-06-17' + }) + }) + expect(status.phases.postOvulatory).to.eql({ + start: { + date: '2018-06-17', + time: '18:00' + }, + cycleDays: mucusPeakSlightlyBeforeTempShift + .filter(({date}) => date >= '2018-06-17') + }) + }) + it('with another mucus peak 5 days after fHM ignores it', () => { const status = getSensiplanStatus({ cycle: mucusPeak5DaysAfterFhm, From de2233f998b12c1ddfa111e88c1861038a24b804 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Fri, 13 Jul 2018 09:09:45 +0200 Subject: [PATCH 40/59] Fix assert for bleeding value === null --- lib/sympto/index.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 2005f32..0150939 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -5,7 +5,7 @@ import { LocalDate } from 'js-joda' import assert from 'assert' export default function getSymptoThermalStatus({ cycle, previousCycles = [] }) { - throwIfArgsAreNotInRequiredFormat(cycle, previousCycles) + throwIfArgsAreNotInRequiredFormat([cycle, ...previousCycles]) const status = { assumeFertility: true, @@ -14,12 +14,14 @@ export default function getSymptoThermalStatus({ cycle, previousCycles = [] }) { // if there was no first higher measurement in the previous cycle, // no infertile pre-ovulatory phase may be assumed - const lastCycle = previousCycles[previousCycles.length - 1] - if (lastCycle && getSymptoThermalStatus({cycle: lastCycle}).temperatureShift) { - status.phases.preOvulatory = getPreOvulatoryPhase(cycle, previousCycles) - if (status.phases.preOvulatory.cycleDays.length === cycle.length) { - status.assumeFertility = false - return status + if (previousCycles) { + const lastCycle = previousCycles[previousCycles.length - 1] + if (lastCycle && getSymptoThermalStatus({ cycle: lastCycle }).temperatureShift) { + status.phases.preOvulatory = getPreOvulatoryPhase(cycle, previousCycles) + if (status.phases.preOvulatory.cycleDays.length === cycle.length) { + status.assumeFertility = false + return status + } } } @@ -71,11 +73,11 @@ export default function getSymptoThermalStatus({ cycle, previousCycles = [] }) { return status } -function throwIfArgsAreNotInRequiredFormat(cycle, previousCycles) { - [cycle, ...previousCycles].forEach(cycle => { +function throwIfArgsAreNotInRequiredFormat(cycles) { + cycles.forEach(cycle => { assert.ok(Array.isArray(cycle)) - // TODO handle case of no previous cycles assert.ok(cycle.length > 0) + assert.ok(cycle[0].bleeding !== null) assert.equal(typeof cycle[0].bleeding, 'object') assert.equal(typeof cycle[0].bleeding.value, 'number') cycle.forEach(day => { From 8a8b1310644f17cc5628e45bb048b35e380118af Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Fri, 13 Jul 2018 09:10:00 +0200 Subject: [PATCH 41/59] Use new smpto module in day view --- components/cycle-day.js | 15 +++++++++++---- lib/cycle.js | 16 ++++++++++++++-- lib/sympto-adapter.js | 31 ++++++++++--------------------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/components/cycle-day.js b/components/cycle-day.js index aafb099..194b145 100644 --- a/components/cycle-day.js +++ b/components/cycle-day.js @@ -4,7 +4,7 @@ import { Text } from 'react-native' import cycleModule from '../lib/cycle' -import { getTemperatureFertilityStatus } from '../lib/sympto-adapter' +import getFertilityStatus from '../lib/sympto-adapter' import DayView from './cycle-day-overview' import BleedingEditView from './bleeding' import TemperatureEditView from './temperature' @@ -29,7 +29,7 @@ export default class Day extends Component { render() { const cycleDayNumber = getCycleDayNumber(this.cycleDay.date) - const temperatureFertilityStatus = getTemperatureFertilityStatus(this.cycleDay.date) + const fertilityStatus = getFertilityStatus(this.cycleDay.date) return ( @@ -38,8 +38,15 @@ export default class Day extends Component { - { cycleDayNumber && Cycle day {cycleDayNumber} } - { cycleDayNumber && Temperature status: {temperatureFertilityStatus} } + { cycleDayNumber && + + Cycle day {cycleDayNumber} + } + + { cycleDayNumber && + + {fertilityStatus} + } { diff --git a/lib/cycle.js b/lib/cycle.js index ec33427..15fca15 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -4,16 +4,20 @@ const LocalDate = joda.LocalDate export default function config(opts) { let bleedingDaysSortedByDate let temperatureDaysSortedByDate + let cycleDaysSortedByDate let maxBreakInBleeding if (!opts) { - // we only want to require (and run) the db module when not running the tests + // we only want to require (and run) the db module + // when not running the tests bleedingDaysSortedByDate = require('../db').bleedingDaysSortedByDate temperatureDaysSortedByDate = require('../db').temperatureDaysSortedByDate + cycleDaysSortedByDate = require('../db').cycleDaysSortedByDate maxBreakInBleeding = 1 } else { bleedingDaysSortedByDate = opts.bleedingDaysSortedByDate || [] temperatureDaysSortedByDate = opts.temperatureDaysSortedByDate || [] + cycleDaysSortedByDate = opts.cycleDaysSortedByDate || [] maxBreakInBleeding = opts.maxBreakInBleeding || 1 } @@ -67,9 +71,17 @@ export default function config(opts) { .map(day => day.temperature.value) } + function getCycleDaysBeforeDay(targetDateString) { + const firstCycleDay = getLastMensesStart(targetDateString) + return cycleDaysSortedByDate.filter(({date}) => { + return date >= firstCycleDay.date && date <= targetDateString + }) + } + return { getCycleDayNumber, getLastMensesStart, - getPreviousTemperaturesInCycle + getPreviousTemperaturesInCycle, + getCycleDaysBeforeDay } } diff --git a/lib/sympto-adapter.js b/lib/sympto-adapter.js index d2f1ffb..e9e384d 100644 --- a/lib/sympto-adapter.js +++ b/lib/sympto-adapter.js @@ -1,29 +1,18 @@ -import getTemperatureStatus from './sympto/temperature' +import getFertilityStatus from './sympto' import cycleModule from './cycle' -const getLastMensesStart = cycleModule().getLastMensesStart -const getPreviousTemperaturesInCycle = cycleModule().getPreviousTemperaturesInCycle +const { getCycleDaysBeforeDay } = cycleModule() + +export default function (dateString) { + // we get earliest last, but sympto wants earliest first + const cycle = getCycleDaysBeforeDay(dateString).reverse() + // const previousCycles = getPreviousCycles() + const status = getFertilityStatus({cycle}) -function getTemperatureFertilityStatus(targetDateString) { - const lastMensesStart = getLastMensesStart(targetDateString) - if (!lastMensesStart) return formatStatusForApp({ detected: false }) - const previousTemperaturesInCycle = getPreviousTemperaturesInCycle(targetDateString, lastMensesStart) - // we get temps with latest first, but sympto module expects latest last - previousTemperaturesInCycle.reverse() - const status = getTemperatureStatus(previousTemperaturesInCycle) return formatStatusForApp(status) } function formatStatusForApp(status) { - if (!status.detected) return 'fertile' - const dict = [ - "regular temperature", - "first exception", - "second exception" - ] - return `infertile according to the ${dict[status.rule]} rule` -} - -export { - getTemperatureFertilityStatus + const fertileStatus = status.assumeFertility ? 'fertile' : 'infertile' + return `You are currently ${fertileStatus}` } \ No newline at end of file From 85e2703b2fdb5c0fed6e3288dbab77cadccc86c1 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Fri, 13 Jul 2018 11:30:54 +0200 Subject: [PATCH 42/59] Get previous cycles before detecting fertility status --- components/cycle-day.js | 3 +- lib/cycle.js | 30 ++++++- lib/sympto-adapter.js | 16 ++-- test/cycle.spec.js | 186 +++++++++++++++++++++++++++++----------- 4 files changed, 175 insertions(+), 60 deletions(-) diff --git a/components/cycle-day.js b/components/cycle-day.js index 194b145..bc3a674 100644 --- a/components/cycle-day.js +++ b/components/cycle-day.js @@ -43,10 +43,9 @@ export default class Day extends Component { Cycle day {cycleDayNumber} } - { cycleDayNumber && {fertilityStatus} - } + { diff --git a/lib/cycle.js b/lib/cycle.js index 15fca15..b5a1e21 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -1,4 +1,5 @@ import * as joda from 'js-joda' + const LocalDate = joda.LocalDate export default function config(opts) { @@ -49,14 +50,16 @@ export default function config(opts) { return !previousBleedingDays.some(({ wrappedDate }) => wrappedDate.equals(periodThreshold) || wrappedDate.isAfter(periodThreshold)) } + withWrappedDates.forEach(day => delete day.wrappedDate) return lastPeriodStart } function getCycleDayNumber(targetDateString) { const lastMensesStart = getLastMensesStart(targetDateString) if (!lastMensesStart) return null - const targetDate = joda.LocalDate.parse(targetDateString) - const diffInDays = lastMensesStart.wrappedDate.until(targetDate, joda.ChronoUnit.DAYS) + const targetDate = LocalDate.parse(targetDateString) + const lastMensesLocalDate = LocalDate.parse(lastMensesStart.date) + const diffInDays = lastMensesLocalDate.until(targetDate, joda.ChronoUnit.DAYS) // cycle starts at day 1 return diffInDays + 1 @@ -73,15 +76,36 @@ export default function config(opts) { function getCycleDaysBeforeDay(targetDateString) { const firstCycleDay = getLastMensesStart(targetDateString) + if (!firstCycleDay) return null return cycleDaysSortedByDate.filter(({date}) => { return date >= firstCycleDay.date && date <= targetDateString }) } + function getPreviousCycles(targetCycleStartDay) { + let previousCycleStartIndex = cycleDaysSortedByDate.indexOf(targetCycleStartDay) + const cycles = [] + while (previousCycleStartIndex < cycleDaysSortedByDate.length - 1) { + const prevDate = cycleDaysSortedByDate[previousCycleStartIndex + 1].date + const cycleStart = getLastMensesStart(prevDate) + + if (!cycleStart) break + + const cycleStartIndex = cycleDaysSortedByDate.indexOf(cycleStart) + const lastDayInCycle = previousCycleStartIndex + 1 + const cycle = cycleDaysSortedByDate.slice(lastDayInCycle, cycleStartIndex + 1) + cycles.push(cycle) + previousCycleStartIndex = cycleStartIndex + } + + return cycles + } + return { getCycleDayNumber, getLastMensesStart, getPreviousTemperaturesInCycle, - getCycleDaysBeforeDay + getCycleDaysBeforeDay, + getPreviousCycles } } diff --git a/lib/sympto-adapter.js b/lib/sympto-adapter.js index e9e384d..8c1c3e1 100644 --- a/lib/sympto-adapter.js +++ b/lib/sympto-adapter.js @@ -1,18 +1,22 @@ import getFertilityStatus from './sympto' import cycleModule from './cycle' -const { getCycleDaysBeforeDay } = cycleModule() +const { getCycleDaysBeforeDay, getPreviousCycles } = cycleModule() export default function (dateString) { + const cycle = getCycleDaysBeforeDay(dateString) + if (!cycle) return `We cannot show any cycle information because no menses has been entered` + // we get earliest last, but sympto wants earliest first - const cycle = getCycleDaysBeforeDay(dateString).reverse() - // const previousCycles = getPreviousCycles() - const status = getFertilityStatus({cycle}) + cycle.reverse() + const previousCycles = getPreviousCycles(cycle[0]) + previousCycles.forEach(cycle => cycle.reverse()) + + const status = getFertilityStatus({cycle, previousCycles}) return formatStatusForApp(status) } function formatStatusForApp(status) { - const fertileStatus = status.assumeFertility ? 'fertile' : 'infertile' - return `You are currently ${fertileStatus}` + return status.assumeFertility ? 'fertile' : 'infertile' } \ No newline at end of file diff --git a/test/cycle.spec.js b/test/cycle.spec.js index ae60632..4b7858c 100644 --- a/test/cycle.spec.js +++ b/test/cycle.spec.js @@ -23,7 +23,7 @@ describe('getCycleDay', () => { value: 2 } }] - const getCycleDayNumber = cycleModule({bleedingDaysSortedByDate: bleedingDays}).getCycleDayNumber + const getCycleDayNumber = cycleModule({ bleedingDaysSortedByDate: bleedingDays }).getCycleDayNumber const targetDate = '2018-05-17' const result = getCycleDayNumber(targetDate) expect(result).to.eql(9) @@ -49,7 +49,7 @@ describe('getCycleDay', () => { } }] const targetDate = '2018-05-17' - const getCycleDayNumber = cycleModule({bleedingDaysSortedByDate: bleedingDays}).getCycleDayNumber + const getCycleDayNumber = cycleModule({ bleedingDaysSortedByDate: bleedingDays }).getCycleDayNumber const result = getCycleDayNumber(targetDate) expect(result).to.eql(15) }) @@ -73,7 +73,7 @@ describe('getCycleDay', () => { }] const targetDate = '2018-04-27' - const getCycleDayNumber = cycleModule({bleedingDaysSortedByDate: bleedingDays}).getCycleDayNumber + const getCycleDayNumber = cycleModule({ bleedingDaysSortedByDate: bleedingDays }).getCycleDayNumber const result = getCycleDayNumber(targetDate) expect(result).to.eql(18) }) @@ -87,59 +87,147 @@ describe('getCycleDay', () => { }] const targetDate = '2018-05-13' - const getCycleDayNumber = cycleModule({bleedingDaysSortedByDate: bleedingDays}).getCycleDayNumber + const getCycleDayNumber = cycleModule({ bleedingDaysSortedByDate: bleedingDays }).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 = cycleModule({bleedingDaysSortedByDate: bleedingDays}).getCycleDayNumber - const result = getCycleDayNumber(targetDate) - expect(result).to.be.null() + describe('getCycleDay returns null', () => { + it('if there are no bleeding days', function () { + const bleedingDays = [] + const targetDate = '2018-05-17' + const getCycleDayNumber = cycleModule({ bleedingDaysSortedByDate: bleedingDays }).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('getCycleDay with cycle thresholds', () => { - const maxBreakInBleeding = 3 +describe('getPreviousCycles', () => { + it('gets previous cycles', () => { + 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 } + }, + ] - 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) + const { getPreviousCycles } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const result = getPreviousCycles(cycleDaysSortedByDate[0]) + expect(result.length).to.eql(3) + expect(result).to.eql([ + [ + { + 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 } + }, + ] + ]) }) }) \ No newline at end of file From 18d6a8c05afcf1c006b9b5f7be40cb038ca41620 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Fri, 13 Jul 2018 12:14:08 +0200 Subject: [PATCH 43/59] Cleanup --- .eslintrc | 2 +- labels/labels.js | 8 +++++--- lib/sympto-adapter.js | 9 +++++++-- lib/sympto/index.js | 30 +++++++++++++++++++----------- lib/sympto/mucus.js | 6 +++--- lib/sympto/temperature.js | 8 ++++---- 6 files changed, 39 insertions(+), 24 deletions(-) diff --git a/.eslintrc b/.eslintrc index 40d7e9b..6ec6205 100644 --- a/.eslintrc +++ b/.eslintrc @@ -48,6 +48,6 @@ "prefer-const": "error", "no-trailing-spaces": "error", "react/prop-types": 0, - "max-len": "warn" + "max-len": [1, {"ignoreStrings": true}] } } \ No newline at end of file diff --git a/labels/labels.js b/labels/labels.js index a5ed61a..9afc142 100644 --- a/labels/labels.js +++ b/labels/labels.js @@ -1,5 +1,7 @@ -const bleeding = ['spotting', 'light', 'medium', 'heavy'] +export const bleeding = ['spotting', 'light', 'medium', 'heavy'] -export { - bleeding +export const fertilityStatus = { + fertile: 'fertile', + infertile: 'infertile', + unknown: 'We cannot show any cycle information because no menses has been entered' } \ No newline at end of file diff --git a/lib/sympto-adapter.js b/lib/sympto-adapter.js index 8c1c3e1..6d2a489 100644 --- a/lib/sympto-adapter.js +++ b/lib/sympto-adapter.js @@ -1,11 +1,12 @@ import getFertilityStatus from './sympto' import cycleModule from './cycle' +import { fertilityStatus } from '../labels/labels' const { getCycleDaysBeforeDay, getPreviousCycles } = cycleModule() export default function (dateString) { const cycle = getCycleDaysBeforeDay(dateString) - if (!cycle) return `We cannot show any cycle information because no menses has been entered` + if (!cycle) return fertilityStatus.unknown // we get earliest last, but sympto wants earliest first cycle.reverse() @@ -18,5 +19,9 @@ export default function (dateString) { } function formatStatusForApp(status) { - return status.assumeFertility ? 'fertile' : 'infertile' + if (status.assumeFertility) { + return fertilityStatus.fertile + } else { + return fertilityStatus.infertile + } } \ No newline at end of file diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 0150939..4290201 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -14,9 +14,10 @@ export default function getSymptoThermalStatus({ cycle, previousCycles = [] }) { // if there was no first higher measurement in the previous cycle, // no infertile pre-ovulatory phase may be assumed - if (previousCycles) { - const lastCycle = previousCycles[previousCycles.length - 1] - if (lastCycle && getSymptoThermalStatus({ cycle: lastCycle }).temperatureShift) { + const lastCycle = previousCycles[previousCycles.length - 1] + if (lastCycle) { + const statusForLast = getSymptoThermalStatus({ cycle: lastCycle }) + if (statusForLast.temperatureShift) { status.phases.preOvulatory = getPreOvulatoryPhase(cycle, previousCycles) if (status.phases.preOvulatory.cycleDays.length === cycle.length) { status.assumeFertility = false @@ -33,7 +34,8 @@ export default function getSymptoThermalStatus({ cycle, previousCycles = [] }) { if (status.phases.preOvulatory) { const prePhase = status.phases.preOvulatory - periPhase.start.date = LocalDate.parse(prePhase.end.date).plusDays(1).toString() + const startDate = LocalDate.parse(prePhase.end.date).plusDays(1).toString() + periPhase.start.date = startDate const lastPreDay = prePhase.cycleDays[prePhase.cycleDays.length - 1] periPhase.cycleDays = cycle.slice(cycle.indexOf(lastPreDay) + 1) } else { @@ -48,22 +50,28 @@ export default function getSymptoThermalStatus({ cycle, previousCycles = [] }) { const mucusShift = getMucusShift(cycle, tempEvalEndIndex) if (!mucusShift.detected) return status - const periOvulatoryEnd = - temperatureShift.evaluationCompleteDay.date > mucusShift.evaluationCompleteDay.date ? - temperatureShift.evaluationCompleteDay : mucusShift.evaluationCompleteDay + let periOvulatoryEnd + const tempOver = temperatureShift.evaluationCompleteDay.date + const mucusOver = mucusShift.evaluationCompleteDay.date - const prevPeriOvulatoryDays = periPhase.cycleDays - const periOvulatoryEndIndex = prevPeriOvulatoryDays.indexOf(periOvulatoryEnd) + if (tempOver > mucusOver) { + periOvulatoryEnd = temperatureShift.evaluationCompleteDay + } else { + periOvulatoryEnd = mucusShift.evaluationCompleteDay + } + + const previousPeriDays = periPhase.cycleDays + const previousPeriEndIndex = previousPeriDays.indexOf(periOvulatoryEnd) status.phases.postOvulatory = { start: { date: periOvulatoryEnd.date, time: '18:00' }, - cycleDays: prevPeriOvulatoryDays.slice(periOvulatoryEndIndex) + cycleDays: previousPeriDays.slice(previousPeriEndIndex) } - periPhase.cycleDays = prevPeriOvulatoryDays.slice(0, periOvulatoryEndIndex + 1) + periPhase.cycleDays = previousPeriDays.slice(0, previousPeriEndIndex + 1) periPhase.end = status.phases.postOvulatory.start status.mucusShift = mucusShift diff --git a/lib/sympto/mucus.js b/lib/sympto/mucus.js index 07a7ca4..9eb37ef 100644 --- a/lib/sympto/mucus.js +++ b/lib/sympto/mucus.js @@ -6,16 +6,16 @@ export default function (cycleDays, tempEvalEndIndex) { const day = mucusDays[i] if (day.mucus.value !== bestQuality) continue - // sensiplan says the three following days must be of lower quality + // the three following days must be of lower quality // AND no best quality day may occur until temperature evaluation has // been completed const threeFollowingDays = mucusDays.slice(i + 1, i + 4) if (threeFollowingDays.length < 3) continue - const bestQualityOccurringIn3FollowingDays = threeFollowingDays.some(day => { + const bestQualityOccursIn3FollowingDays = threeFollowingDays.some(day => { return day.mucus.value >= bestQuality }) - if (bestQualityOccurringIn3FollowingDays) continue + if (bestQualityOccursIn3FollowingDays) continue const cycleDayIndex = cycleDays.indexOf(day) const relevantDays = cycleDays diff --git a/lib/sympto/temperature.js b/lib/sympto/temperature.js index af90282..387cd63 100644 --- a/lib/sympto/temperature.js +++ b/lib/sympto/temperature.js @@ -23,11 +23,11 @@ export default function (cycleDays) { const temp = temperatureDays[i].temp if (temp <= ltl) continue - const checkResult = checkIfFirstHighMeasurement(temp, i, temperatureDays, ltl) + const shift = checkIfFirstHighMeasurement(temp, i, temperatureDays, ltl) - if (checkResult.detected) { - checkResult.firstHighMeasurementDay = temperatureDays[i].originalCycleDay - return checkResult + if (shift.detected) { + shift.firstHighMeasurementDay = temperatureDays[i].originalCycleDay + return shift } } From 79f17644d701ab49de65a1b45ea6b1bd144d71d5 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Mon, 16 Jul 2018 18:21:58 +0200 Subject: [PATCH 44/59] ComputedNfp => value --- components/cycle-day/cycle-day-overview.js | 2 +- db.js | 2 +- lib/sympto-adapter.js | 14 ++++++++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/components/cycle-day/cycle-day-overview.js b/components/cycle-day/cycle-day-overview.js index d43aab4..a4174a4 100644 --- a/components/cycle-day/cycle-day-overview.js +++ b/components/cycle-day/cycle-day-overview.js @@ -62,7 +62,7 @@ export default class DayView extends Component { const mucusFeelingValue = this.cycleDay.mucus && this.cycleDay.mucus.feeling const mucusTextureValue = this.cycleDay.mucus && this.cycleDay.mucus.texture - const mucusComputedValue = this.cycleDay.mucus && this.cycleDay.mucus.computedNfp + const mucusComputedValue = this.cycleDay.mucus && this.cycleDay.mucus.value let mucusLabel if (typeof mucusFeelingValue === 'number' && typeof mucusTextureValue === 'number') { mucusLabel = `${feelingLabels[mucusFeelingValue]} + ${textureLabels[mucusTextureValue]} ( ${computeSensiplanMucusLabels[mucusComputedValue]} )` diff --git a/db.js b/db.js index c0533bf..7f5e3e5 100644 --- a/db.js +++ b/db.js @@ -23,7 +23,7 @@ const MucusSchema = { properties: { feeling: 'int', texture: 'int', - computedNfp: 'int', + value: 'int', exclude: 'bool' } } diff --git a/lib/sympto-adapter.js b/lib/sympto-adapter.js index 6d2a489..79a4742 100644 --- a/lib/sympto-adapter.js +++ b/lib/sympto-adapter.js @@ -8,12 +8,12 @@ export default function (dateString) { const cycle = getCycleDaysBeforeDay(dateString) if (!cycle) return fertilityStatus.unknown - // we get earliest last, but sympto wants earliest first - cycle.reverse() const previousCycles = getPreviousCycles(cycle[0]) - previousCycles.forEach(cycle => cycle.reverse()) - const status = getFertilityStatus({cycle, previousCycles}) + const status = getFertilityStatus({ + cycle: formatCycleForSympto(cycle), + previousCycles: previousCycles.map(formatCycleForSympto) + }) return formatStatusForApp(status) } @@ -24,4 +24,10 @@ function formatStatusForApp(status) { } else { return fertilityStatus.infertile } +} + +function formatCycleForSympto(cycle) { + // we get earliest last, but sympto wants earliest first + cycle.reverse() + return cycle } \ No newline at end of file From b455a030cc6abbbb4e48b60a7a799c5befe8d01d Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Mon, 16 Jul 2018 18:39:40 +0200 Subject: [PATCH 45/59] Label last day of post-ovu phase differently --- labels/labels.js | 1 + lib/sympto-adapter.js | 9 ++++++--- styles/index.js | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/labels/labels.js b/labels/labels.js index 9afc142..20c8ad0 100644 --- a/labels/labels.js +++ b/labels/labels.js @@ -3,5 +3,6 @@ export const bleeding = ['spotting', 'light', 'medium', 'heavy'] export const fertilityStatus = { fertile: 'fertile', infertile: 'infertile', + fertileUntilEvening: 'Fertile phase ends in the evening', unknown: 'We cannot show any cycle information because no menses has been entered' } \ No newline at end of file diff --git a/lib/sympto-adapter.js b/lib/sympto-adapter.js index 79a4742..0d354cb 100644 --- a/lib/sympto-adapter.js +++ b/lib/sympto-adapter.js @@ -15,10 +15,13 @@ export default function (dateString) { previousCycles: previousCycles.map(formatCycleForSympto) }) - return formatStatusForApp(status) -} + if (status.phases.postOvulatory) { + const phase = status.phases.postOvulatory + if (phase.start.date === dateString) { + return fertilityStatus.fertileUntilEvening + } + } -function formatStatusForApp(status) { if (status.assumeFertility) { return fertilityStatus.fertile } else { diff --git a/styles/index.js b/styles/index.js index 52fc65d..1b09eb4 100644 --- a/styles/index.js +++ b/styles/index.js @@ -21,7 +21,6 @@ export default StyleSheet.create({ }, cycleDayNumber: { fontSize: 18, - margin: 15, textAlign: 'center', textAlignVertical: 'center' }, @@ -57,7 +56,8 @@ export default StyleSheet.create({ cycleDayNumberView: { justifyContent: 'center', backgroundColor: 'skyblue', - marginBottom: 15 + marginBottom: 15, + paddingVertical: 15 }, homeButtons: { marginHorizontal: 15 From 52b6e5de0a0b6c5fc39ed25741a95a4f140aff48 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Wed, 18 Jul 2018 10:02:11 +0200 Subject: [PATCH 46/59] Make previousCycle explicit in signature --- lib/sympto/index.js | 11 ++++----- test/sympto/index.spec.js | 50 ++++++++++++++++++++++----------------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 4290201..2163c91 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -4,8 +4,8 @@ import getPreOvulatoryPhase from './pre-ovulatory' import { LocalDate } from 'js-joda' import assert from 'assert' -export default function getSymptoThermalStatus({ cycle, previousCycles = [] }) { - throwIfArgsAreNotInRequiredFormat([cycle, ...previousCycles]) +export default function getSymptoThermalStatus({ cycle, previousCycle, earlierCycles = [] }) { + throwIfArgsAreNotInRequiredFormat([cycle, ...earlierCycles]) const status = { assumeFertility: true, @@ -14,11 +14,10 @@ export default function getSymptoThermalStatus({ cycle, previousCycles = [] }) { // if there was no first higher measurement in the previous cycle, // no infertile pre-ovulatory phase may be assumed - const lastCycle = previousCycles[previousCycles.length - 1] - if (lastCycle) { - const statusForLast = getSymptoThermalStatus({ cycle: lastCycle }) + if (previousCycle) { + const statusForLast = getSymptoThermalStatus({ cycle: previousCycle }) if (statusForLast.temperatureShift) { - status.phases.preOvulatory = getPreOvulatoryPhase(cycle, previousCycles) + status.phases.preOvulatory = getPreOvulatoryPhase(cycle, [previousCycle, ...earlierCycles]) if (status.phases.preOvulatory.cycleDays.length === cycle.length) { status.assumeFertility = false return status diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index b9dcd9f..4f805e0 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -26,7 +26,7 @@ describe('sympto', () => { it('with no shifts detects only peri-ovulatory', function () { const status = getSensiplanStatus({ cycle: cycleWithoutAnyShifts, - previousCycles: [cycleWithoutFhm,] + previousCycle: cycleWithoutFhm }) expect(status).to.eql({ @@ -43,7 +43,7 @@ describe('sympto', () => { it('with shifts detects only peri-ovulatory and post-ovulatory', () => { const status = getSensiplanStatus({ cycle: longAndComplicatedCycle, - previousCycles: [cycleWithoutFhm,] + previousCycle: cycleWithoutFhm }) expect(status.temperatureShift).to.be.an('object') @@ -71,7 +71,7 @@ describe('sympto', () => { it('according to 5-day-rule', function () { const status = getSensiplanStatus({ cycle: fiveDayCycle, - previousCycles: [cycleWithFhm] + previousCycle: cycleWithFhm }) expect(Object.keys(status.phases).length).to.eql(1) @@ -88,7 +88,7 @@ describe('sympto', () => { it('according to 5-day-rule', function () { const status = getSensiplanStatus({ cycle: cycleWithTempAndNoMucusShift, - previousCycles: [cycleWithFhm] + previousCycle: cycleWithFhm }) expect(Object.keys(status.phases).length).to.eql(2) @@ -108,7 +108,7 @@ describe('sympto', () => { it('according to 5-day-rule with shortened pre-phase', function () { const status = getSensiplanStatus({ cycle: cycleWithEarlyMucus, - previousCycles: [cycleWithFhm] + previousCycle: cycleWithFhm }) expect(Object.keys(status.phases).length).to.eql(2) @@ -128,7 +128,7 @@ describe('sympto', () => { it('according to 5-day-rule', function () { const status = getSensiplanStatus({ cycle: longAndComplicatedCycle, - previousCycles: [cycleWithFhm] + previousCycle: cycleWithFhm }) expect(Object.keys(status.phases).length).to.eql(3) @@ -159,7 +159,7 @@ describe('sympto', () => { it('with fhM + mucus peak on same day finds start of postovu phase', () => { const status = getSensiplanStatus({ cycle: mucusPeakAndFhmOnSameDay, - previousCycles: [cycleWithFhm] + previousCycle: cycleWithFhm }) expect(status.temperatureShift).to.be.an('object') @@ -193,7 +193,7 @@ describe('sympto', () => { it('with fhM 2 days before mucus peak waits for end of mucus eval', () => { const status = getSensiplanStatus({ cycle: fhmTwoDaysBeforeMucusPeak, - previousCycles: [cycleWithFhm] + previousCycle: cycleWithFhm }) expect(status.temperatureShift).to.be.an('object') @@ -227,7 +227,7 @@ describe('sympto', () => { it('another example for mucus peak before temp shift', () => { const status = getSensiplanStatus({ cycle: mucusPeakSlightlyBeforeTempShift, - previousCycles: [cycleWithFhm] + previousCycle: cycleWithFhm }) expect(status.temperatureShift).to.be.an('object') @@ -261,7 +261,7 @@ describe('sympto', () => { it('with another mucus peak 5 days after fHM ignores it', () => { const status = getSensiplanStatus({ cycle: mucusPeak5DaysAfterFhm, - previousCycles: [cycleWithFhm] + previousCycle: cycleWithFhm }) expect(status.temperatureShift).to.be.an('object') @@ -295,7 +295,7 @@ describe('sympto', () => { it('with mucus peak 2 days before fhM waits for end of temp eval', () => { const status = getSensiplanStatus({ cycle: mucusPeakTwoDaysBeforeFhm, - previousCycles: [cycleWithFhm] + previousCycle: cycleWithFhm }) expect(status.temperatureShift).to.be.an('object') @@ -329,7 +329,7 @@ describe('sympto', () => { it('with mucus peak 5 days before fhM waits for end of temp eval', () => { const status = getSensiplanStatus({ cycle: fhm5DaysAfterMucusPeak, - previousCycles: [cycleWithFhm] + previousCycle: cycleWithFhm }) expect(status.temperatureShift).to.be.an('object') @@ -365,7 +365,8 @@ describe('sympto', () => { it('shortens the pre-ovu phase if there is a previous <13 fhm', () => { const status = getSensiplanStatus({ cycle: longAndComplicatedCycle, - previousCycles: [fhmOnDay12, ...Array(13).fill(fhmOnDay15)] + previousCycle: fhmOnDay15, + earlierCycles: [fhmOnDay12, ...Array(10).fill(fhmOnDay15)] }) expect(status.temperatureShift).to.be.an('object') @@ -398,7 +399,8 @@ describe('sympto', () => { it('shortens pre-ovu phase with prev <13 fhm even with <12 cycles', () => { const status = getSensiplanStatus({ cycle: longAndComplicatedCycle, - previousCycles: Array(11).fill(fhmOnDay12) + previousCycle: fhmOnDay12, + earlierCycles: Array(10).fill(fhmOnDay12) }) expect(status.temperatureShift).to.be.an('object') @@ -431,7 +433,8 @@ describe('sympto', () => { it('shortens the pre-ovu phase if mucus occurs', () => { const status = getSensiplanStatus({ cycle: cycleWithEarlyMucus, - previousCycles: Array(11).fill(fhmOnDay12) + previousCycle: fhmOnDay12, + earlierCycles: Array(10).fill(fhmOnDay12) }) expect(status.assumeFertility).to.be.true() @@ -453,7 +456,8 @@ describe('sympto', () => { it('lengthens the pre-ovu phase if >= 12 cycles with fhm > 13', () => { const status = getSensiplanStatus({ cycle: longAndComplicatedCycle, - previousCycles: Array(12).fill(fhmOnDay15) + previousCycle: fhmOnDay15, + earlierCycles: Array(11).fill(fhmOnDay15) }) expect(status.assumeFertility).to.be.false() @@ -485,7 +489,8 @@ describe('sympto', () => { it('does not lengthen the pre-ovu phase if < 12 cycles', () => { const status = getSensiplanStatus({ cycle: longAndComplicatedCycle, - previousCycles: Array(11).fill(fhmOnDay15) + previousCycle: fhmOnDay15, + earlierCycles: Array(10).fill(fhmOnDay15) }) expect(status.assumeFertility).to.be.false() @@ -517,7 +522,8 @@ describe('sympto', () => { it('does not detect any pre-ovu phase if prev cycle had no fhm', () => { const status = getSensiplanStatus({ cycle: longAndComplicatedCycle, - previousCycles: [...Array(12).fill(fhmOnDay15), cycleWithoutFhm] + previousCycle: cycleWithoutFhm, + earlierCycles: [...Array(12).fill(fhmOnDay15)] }) expect(status.assumeFertility).to.be.false() @@ -555,7 +561,7 @@ describe('sympto', () => { hello: 'world', bleeding: { value: 0 } }], - previousCycles: [[{ + earlierCycles: [[{ date: '1992-09-09', bleeding: { value: 0 } }]] @@ -566,7 +572,7 @@ describe('sympto', () => { temperature: {value: '35'}, bleeding: { value: 0 } }], - previousCycles: [[{ + earlierCycles: [[{ date: '1992-09-09', bleeding: { value: 0 } }]] @@ -576,7 +582,7 @@ describe('sympto', () => { date: '09-14-2017', bleeding: { value: 0 } }], - previousCycles: [[{ + earlierCycles: [[{ date: '1992-09-09', bleeding: { value: 0 } }]] @@ -590,7 +596,7 @@ describe('sympto', () => { value: 'medium' } }], - previousCycles: [[ + earlierCycles: [[ { date: '2017-09-23', } From 1fb6c24629f5ccaa1321a34f9d7e044918c53707 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Wed, 18 Jul 2018 13:31:44 +0200 Subject: [PATCH 47/59] Update directories to be linted --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 159a9f2..21a8c26 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "log": "./node_modules/.bin/react-native log-android | grep -v 'Warning: isMounted(...) is deprecated'", "test": "mocha --recursive --require babel-core/register test && npm run lint", "test-watch": "mocha --recursive --require babel-core/register --watch test", - "lint": "eslint app test" + "lint": "eslint components lib test" }, "dependencies": { "assert": "^1.4.1", From 42a96da69d9444f7cb38d7a65640f59cae045093 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Wed, 18 Jul 2018 13:32:40 +0200 Subject: [PATCH 48/59] Change cycle module API, add getCycleForDay, remove unused functions --- lib/cycle.js | 100 ++++++++++++++++++++++++----------------- test/cycle.spec.js | 109 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 165 insertions(+), 44 deletions(-) diff --git a/lib/cycle.js b/lib/cycle.js index b5a1e21..f60a74f 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -1,10 +1,9 @@ import * as joda from 'js-joda' - const LocalDate = joda.LocalDate + export default function config(opts) { let bleedingDaysSortedByDate - let temperatureDaysSortedByDate let cycleDaysSortedByDate let maxBreakInBleeding @@ -12,12 +11,10 @@ export default function config(opts) { // we only want to require (and run) the db module // when not running the tests bleedingDaysSortedByDate = require('../db').bleedingDaysSortedByDate - temperatureDaysSortedByDate = require('../db').temperatureDaysSortedByDate cycleDaysSortedByDate = require('../db').cycleDaysSortedByDate maxBreakInBleeding = 1 } else { bleedingDaysSortedByDate = opts.bleedingDaysSortedByDate || [] - temperatureDaysSortedByDate = opts.temperatureDaysSortedByDate || [] cycleDaysSortedByDate = opts.cycleDaysSortedByDate || [] maxBreakInBleeding = opts.maxBreakInBleeding || 1 } @@ -38,20 +35,44 @@ export default function config(opts) { ) }) - if (firstBleedingDayBeforeTargetDayIndex < 0) return null + if (firstBleedingDayBeforeTargetDayIndex < 0) { + withWrappedDates.forEach(day => delete day.wrappedDate) + return null + } + const previousBleedingDays = withWrappedDates.slice(firstBleedingDayBeforeTargetDayIndex) - const lastPeriodStart = previousBleedingDays.find((day, i) => { + const lastMensesStart = previousBleedingDays.find((day, i) => { return thereIsNoPreviousBleedingDayWithinTheThreshold(day, previousBleedingDays.slice(i + 1)) }) function thereIsNoPreviousBleedingDayWithinTheThreshold(bleedingDay, previousBleedingDays) { const periodThreshold = bleedingDay.wrappedDate.minusDays(maxBreakInBleeding + 1) - return !previousBleedingDays.some(({ wrappedDate }) => wrappedDate.equals(periodThreshold) || wrappedDate.isAfter(periodThreshold)) + return !previousBleedingDays.some(({ wrappedDate }) => { + return wrappedDate.equals(periodThreshold) || wrappedDate.isAfter(periodThreshold) + }) } withWrappedDates.forEach(day => delete day.wrappedDate) - return lastPeriodStart + return lastMensesStart + } + + function getFollowingMensesStart(targetDateString) { + const targetDate = LocalDate.parse(targetDateString) + const withWrappedDates = bleedingDaysSortedByDate + .filter(day => !day.bleeding.exclude) + .map(day => { + day.wrappedDate = LocalDate.parse(day.date) + return day + }) + + const firstBleedingDayAfterTargetDay = withWrappedDates.reverse().find(day => { + return day.wrappedDate.isAfter(targetDate) + }) + + withWrappedDates.forEach(day => delete day.wrappedDate) + + return firstBleedingDayAfterTargetDay } function getCycleDayNumber(targetDateString) { @@ -65,47 +86,44 @@ export default function config(opts) { return diffInDays + 1 } - function getPreviousTemperaturesInCycle(targetDateString, lastMensesStart) { - const startIndex = temperatureDaysSortedByDate.findIndex(day => day.date <= targetDateString) - const previousTemperaturesInCycle = temperatureDaysSortedByDate.slice(startIndex) - const endIndex = previousTemperaturesInCycle.findIndex(day => day.date < lastMensesStart.date) - return previousTemperaturesInCycle - .slice(0, endIndex) - .map(day => day.temperature.value) + function getCyclesBefore(targetCycleStartDay) { + return collectPreviousCycles([], targetCycleStartDay.date) } - function getCycleDaysBeforeDay(targetDateString) { - const firstCycleDay = getLastMensesStart(targetDateString) - if (!firstCycleDay) return null - return cycleDaysSortedByDate.filter(({date}) => { - return date >= firstCycleDay.date && date <= targetDateString - }) + 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 getPreviousCycles(targetCycleStartDay) { - let previousCycleStartIndex = cycleDaysSortedByDate.indexOf(targetCycleStartDay) - const cycles = [] - while (previousCycleStartIndex < cycleDaysSortedByDate.length - 1) { - const prevDate = cycleDaysSortedByDate[previousCycleStartIndex + 1].date - const cycleStart = getLastMensesStart(prevDate) + function getPreviousCycle(dateString) { + const startOfCycle = getLastMensesStart(dateString) + if (!startOfCycle) return null + const dateBeforeStartOfCycle = LocalDate.parse(startOfCycle.date).minusDays(1).toString() + return getCycleForDay(dateBeforeStartOfCycle) + } - if (!cycleStart) break - - const cycleStartIndex = cycleDaysSortedByDate.indexOf(cycleStart) - const lastDayInCycle = previousCycleStartIndex + 1 - const cycle = cycleDaysSortedByDate.slice(lastDayInCycle, cycleStartIndex + 1) - cycles.push(cycle) - previousCycleStartIndex = cycleStartIndex + function getCycleForDay(dayOrDate) { + const dateString = typeof dayOrDate === 'string' ? dayOrDate : dayOrDate.date + const cycleStart = getLastMensesStart(dateString) + if (!cycleStart) return null + const cycleStartIndex = cycleDaysSortedByDate.indexOf(cycleStart) + const nextMensesStart = getFollowingMensesStart(dateString) + if (nextMensesStart) { + return cycleDaysSortedByDate.slice( + cycleDaysSortedByDate.indexOf(nextMensesStart) + 1, + cycleStartIndex + 1 + ) + } else { + return cycleDaysSortedByDate.slice(0, cycleStartIndex + 1) } - - return cycles } return { getCycleDayNumber, - getLastMensesStart, - getPreviousTemperaturesInCycle, - getCycleDaysBeforeDay, - getPreviousCycles + getCycleForDay, + getPreviousCycle, + getCyclesBefore } -} +} \ No newline at end of file diff --git a/test/cycle.spec.js b/test/cycle.spec.js index 4b7858c..3b82c40 100644 --- a/test/cycle.spec.js +++ b/test/cycle.spec.js @@ -144,7 +144,7 @@ describe('getCycleDay', () => { }) }) -describe('getPreviousCycles', () => { +describe('getCyclesBefore', () => { it('gets previous cycles', () => { const cycleDaysSortedByDate = [ { @@ -185,11 +185,11 @@ describe('getPreviousCycles', () => { }, ] - const { getPreviousCycles } = cycleModule({ + const { getCyclesBefore } = cycleModule({ cycleDaysSortedByDate, bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) }) - const result = getPreviousCycles(cycleDaysSortedByDate[0]) + const result = getCyclesBefore(cycleDaysSortedByDate[0]) expect(result.length).to.eql(3) expect(result).to.eql([ [ @@ -230,4 +230,107 @@ describe('getPreviousCycles', () => { ] ]) }) +}) + +describe('getCycleForDay', () => { + 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 { getCycleForDay } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + + it('gets cycle that has only one day', () => { + const result = getCycleForDay('2018-07-05') + expect(result.length).to.eql(1) + expect(result).to.eql([ + { + date: '2018-07-05', + bleeding: { value: 2 } + } + ]) + const result2 = getCycleForDay('2018-06-05') + expect(result2.length).to.eql(1) + expect(result2).to.eql([ + { + date: '2018-06-05', + bleeding: { value: 2 } + } + ]) + }) + + it('for later date gets cycle that has only one day', () => { + const result = getCycleForDay('2018-06-20') + expect(result.length).to.eql(1) + expect(result).to.eql([ + { + date: '2018-06-05', + bleeding: { value: 2 } + } + ]) + }) + + it('returns null if there is no cycle start for that date', () => { + const result = getCycleForDay('2018-04-01') + expect(result).to.eql(null) + }) + + it('gets cycle for day', () => { + const result = getCycleForDay('2018-04-04') + expect(result.length).to.eql(4) + expect(result).to.eql([ + { + 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 } + }, + ]) + }) }) \ No newline at end of file From ebf18dac22dff00652c30d20e04f017752ae8f12 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Wed, 18 Jul 2018 14:14:43 +0200 Subject: [PATCH 49/59] Refactor getStatusAsString --- components/cycle-day/index.js | 4 +- components/cycle-day/symptoms/mucus.js | 2 +- lib/sympto-adapter.js | 60 +++++++++++++++++++------- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/components/cycle-day/index.js b/components/cycle-day/index.js index 3eeb983..78c3293 100644 --- a/components/cycle-day/index.js +++ b/components/cycle-day/index.js @@ -4,7 +4,7 @@ import { Text } from 'react-native' import cycleModule from '../../lib/cycle' -import getFertilityStatus from '../../lib/sympto-adapter' +import { getFertilityStatusStringForDay } from '../../lib/sympto-adapter' import DayView from './cycle-day-overview' import BleedingEditView from './symptoms/bleeding' import TemperatureEditView from './symptoms/temperature' @@ -33,7 +33,7 @@ export default class Day extends Component { render() { const cycleDayNumber = getCycleDayNumber(this.cycleDay.date) - const fertilityStatus = getFertilityStatus(this.cycleDay.date) + const fertilityStatus = getFertilityStatusStringForDay(this.cycleDay.date) return ( diff --git a/components/cycle-day/symptoms/mucus.js b/components/cycle-day/symptoms/mucus.js index f94a656..68ff292 100644 --- a/components/cycle-day/symptoms/mucus.js +++ b/components/cycle-day/symptoms/mucus.js @@ -94,7 +94,7 @@ export default class Mucus extends Component { saveSymptom('mucus', this.cycleDay, { feeling: this.state.currentFeelingValue, texture: this.state.currentTextureValue, - computedNfp: computeSensiplanValue(this.state.currentFeelingValue, this.state.currentTextureValue), + value: computeSensiplanValue(this.state.currentFeelingValue, this.state.currentTextureValue), exclude: this.state.exclude }) }, diff --git a/lib/sympto-adapter.js b/lib/sympto-adapter.js index 0d354cb..337a1a7 100644 --- a/lib/sympto-adapter.js +++ b/lib/sympto-adapter.js @@ -2,31 +2,59 @@ import getFertilityStatus from './sympto' import cycleModule from './cycle' import { fertilityStatus } from '../labels/labels' -const { getCycleDaysBeforeDay, getPreviousCycles } = cycleModule() +const { + getCycleForDay, + getCyclesBefore, + getPreviousCycle +} = cycleModule() -export default function (dateString) { - const cycle = getCycleDaysBeforeDay(dateString) +export function getFertilityStatusStringForDay(dateString) { + const cycle = getCycleForDay(dateString) if (!cycle) return fertilityStatus.unknown - const previousCycles = getPreviousCycles(cycle[0]) + const cycleInfo = {cycle: formatCycleForSympto(cycle)} - const status = getFertilityStatus({ - cycle: formatCycleForSympto(cycle), - previousCycles: previousCycles.map(formatCycleForSympto) - }) + const previousCycle = getPreviousCycle(dateString) - if (status.phases.postOvulatory) { - const phase = status.phases.postOvulatory - if (phase.start.date === dateString) { - return fertilityStatus.fertileUntilEvening + if (previousCycle) { + cycleInfo.previousCycle = formatCycleForSympto(previousCycle) + const earlierCycles = getCyclesBefore(previousCycle[0]) + if (earlierCycles) { + cycleInfo.earlierCycles = earlierCycles.map(formatCycleForSympto) } } - if (status.assumeFertility) { - return fertilityStatus.fertile - } else { - return fertilityStatus.infertile + const status = getFertilityStatus(cycleInfo) + + const phaseNameForDay = Object.keys(status.phases).find(phaseName => { + const phase = status.phases[phaseName] + const dayIsAfterPhaseStart = dateString >= phase.start.date + let dayIsBeforePhaseEnd + if (phase.end) { + dayIsBeforePhaseEnd = dateString <= phase.end.date + } else { + dayIsBeforePhaseEnd = true + } + return dayIsAfterPhaseStart && dayIsBeforePhaseEnd + }) + + return mapToString(phaseNameForDay, dateString, status) +} + +function mapToString(phaseNameForDay, dateString, status) { + const mapping = { + preOvulatory: () => fertilityStatus.infertile, + periOvulatory: (dateString, status) => { + const phaseEnd = status.phases.periOvulatory.end + if (phaseEnd && phaseEnd.date === dateString) { + return fertilityStatus.fertileUntilEvening + } + return fertilityStatus.fertile + }, + postOvulatory: () => fertilityStatus.infertile } + + return mapping[phaseNameForDay](dateString, status) } function formatCycleForSympto(cycle) { From 58aa0c4b8d0aca1b32ed1b016bc019b64f7d96f2 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Wed, 18 Jul 2018 14:18:04 +0200 Subject: [PATCH 50/59] Remove assumeFertility from status, derive from phases instead --- lib/sympto/index.js | 3 --- test/sympto/index.spec.js | 36 ++++++++++++++++++------------------ 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index 2163c91..d032965 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -8,7 +8,6 @@ export default function getSymptoThermalStatus({ cycle, previousCycle, earlierCy throwIfArgsAreNotInRequiredFormat([cycle, ...earlierCycles]) const status = { - assumeFertility: true, phases: {} } @@ -19,7 +18,6 @@ export default function getSymptoThermalStatus({ cycle, previousCycle, earlierCy if (statusForLast.temperatureShift) { status.phases.preOvulatory = getPreOvulatoryPhase(cycle, [previousCycle, ...earlierCycles]) if (status.phases.preOvulatory.cycleDays.length === cycle.length) { - status.assumeFertility = false return status } } @@ -75,7 +73,6 @@ export default function getSymptoThermalStatus({ cycle, previousCycle, earlierCy status.mucusShift = mucusShift status.temperatureShift = temperatureShift - status.assumeFertility = false return status } diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index 4f805e0..1908aab 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -30,7 +30,7 @@ describe('sympto', () => { }) expect(status).to.eql({ - assumeFertility: true, + phases: { periOvulatory: { start: { date: '2018-06-01' }, @@ -48,7 +48,7 @@ describe('sympto', () => { expect(status.temperatureShift).to.be.an('object') expect(status.mucusShift).to.be.an('object') - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(2) expect(status.phases.periOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -75,7 +75,7 @@ describe('sympto', () => { }) expect(Object.keys(status.phases).length).to.eql(1) - expect(status.assumeFertility).to.be.false() + expect(status.phases.preOvulatory).to.eql({ cycleDays: fiveDayCycle, start: { date: '2018-06-01' }, @@ -92,7 +92,7 @@ describe('sympto', () => { }) expect(Object.keys(status.phases).length).to.eql(2) - expect(status.assumeFertility).to.be.true() + expect(status.phases.preOvulatory).to.eql({ cycleDays: cycleWithTempAndNoMucusShift .filter(({date}) => date <= '2018-06-05'), @@ -112,7 +112,7 @@ describe('sympto', () => { }) expect(Object.keys(status.phases).length).to.eql(2) - expect(status.assumeFertility).to.be.true() + expect(status.phases.preOvulatory).to.eql({ cycleDays: [cycleWithEarlyMucus[0]], start: { date: '2018-06-01' }, @@ -132,7 +132,7 @@ describe('sympto', () => { }) expect(Object.keys(status.phases).length).to.eql(3) - expect(status.assumeFertility).to.be.false() + expect(status.phases.preOvulatory).to.eql({ cycleDays: longAndComplicatedCycle .filter(({date}) => date <= '2018-06-05'), @@ -164,7 +164,7 @@ describe('sympto', () => { expect(status.temperatureShift).to.be.an('object') expect(status.mucusShift).to.be.an('object') - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -198,7 +198,7 @@ describe('sympto', () => { expect(status.temperatureShift).to.be.an('object') expect(status.mucusShift).to.be.an('object') - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -232,7 +232,7 @@ describe('sympto', () => { expect(status.temperatureShift).to.be.an('object') expect(status.mucusShift).to.be.an('object') - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -266,7 +266,7 @@ describe('sympto', () => { expect(status.temperatureShift).to.be.an('object') expect(status.mucusShift).to.be.an('object') - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -300,7 +300,7 @@ describe('sympto', () => { expect(status.temperatureShift).to.be.an('object') expect(status.mucusShift).to.be.an('object') - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -334,7 +334,7 @@ describe('sympto', () => { expect(status.temperatureShift).to.be.an('object') expect(status.mucusShift).to.be.an('object') - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -371,7 +371,7 @@ describe('sympto', () => { expect(status.temperatureShift).to.be.an('object') expect(status.mucusShift).to.be.an('object') - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -405,7 +405,7 @@ describe('sympto', () => { expect(status.temperatureShift).to.be.an('object') expect(status.mucusShift).to.be.an('object') - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -437,7 +437,7 @@ describe('sympto', () => { earlierCycles: Array(10).fill(fhmOnDay12) }) - expect(status.assumeFertility).to.be.true() + expect(Object.keys(status.phases).length).to.eql(2) expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -460,7 +460,7 @@ describe('sympto', () => { earlierCycles: Array(11).fill(fhmOnDay15) }) - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -493,7 +493,7 @@ describe('sympto', () => { earlierCycles: Array(10).fill(fhmOnDay15) }) - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(3) expect(status.phases.preOvulatory).to.eql({ start: { date: '2018-06-01' }, @@ -526,7 +526,7 @@ describe('sympto', () => { earlierCycles: [...Array(12).fill(fhmOnDay15)] }) - expect(status.assumeFertility).to.be.false() + expect(Object.keys(status.phases).length).to.eql(2) expect(status.phases.periOvulatory).to.eql({ start: { date: '2018-06-01' }, From 67ebacfdd760d57d387919eb5fa27ffb15b02d2b Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Wed, 18 Jul 2018 16:39:52 +0200 Subject: [PATCH 51/59] Fix whitespace --- test/sympto/index.spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index 1908aab..aed8f3a 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -30,7 +30,7 @@ describe('sympto', () => { }) expect(status).to.eql({ - + phases: { periOvulatory: { start: { date: '2018-06-01' }, @@ -75,7 +75,7 @@ describe('sympto', () => { }) expect(Object.keys(status.phases).length).to.eql(1) - + expect(status.phases.preOvulatory).to.eql({ cycleDays: fiveDayCycle, start: { date: '2018-06-01' }, @@ -92,7 +92,7 @@ describe('sympto', () => { }) expect(Object.keys(status.phases).length).to.eql(2) - + expect(status.phases.preOvulatory).to.eql({ cycleDays: cycleWithTempAndNoMucusShift .filter(({date}) => date <= '2018-06-05'), @@ -112,7 +112,7 @@ describe('sympto', () => { }) expect(Object.keys(status.phases).length).to.eql(2) - + expect(status.phases.preOvulatory).to.eql({ cycleDays: [cycleWithEarlyMucus[0]], start: { date: '2018-06-01' }, @@ -132,7 +132,7 @@ describe('sympto', () => { }) expect(Object.keys(status.phases).length).to.eql(3) - + expect(status.phases.preOvulatory).to.eql({ cycleDays: longAndComplicatedCycle .filter(({date}) => date <= '2018-06-05'), From 955207ed8d6d825ad432e4b54238d2626651407c Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 19 Jul 2018 07:17:30 +0200 Subject: [PATCH 52/59] Draw vertical and horizontal line in chart for completed sympto eval --- components/chart/chart.js | 71 ++++++++++++++++++++++++++++++++++++++- lib/sympto-adapter.js | 37 +++++++++++--------- 2 files changed, 91 insertions(+), 17 deletions(-) diff --git a/components/chart/chart.js b/components/chart/chart.js index 877d231..9030bfa 100644 --- a/components/chart/chart.js +++ b/components/chart/chart.js @@ -14,6 +14,7 @@ import { getCycleDay, getOrCreateCycleDay, cycleDaysSortedByDate } from '../../d import cycleModule from '../../lib/cycle' import styles from './styles' import config from './config' +import { getCycleStatusForDay } from '../../lib/sympto-adapter' const getCycleDayNumber = cycleModule().getCycleDayNumber @@ -63,10 +64,21 @@ export default class CycleChart extends Component { const cycleDayNumber = getCycleDayNumber(dateString) const label = styles.column.label const dateLabel = dateString.split('-').slice(1).join('-') + const getFhmAndLtlInfo = setUpFertilityStatusFunc() + const nfpLineInfo = getFhmAndLtlInfo(dateString) return ( this.passDateToDayView(dateString)}> + {nfpLineInfo.drawFhmLine ? + : null} {this.placeHorizontalGrid()} {cycleDayNumber} {dateLabel} @@ -79,6 +91,16 @@ export default class CycleChart extends Component { Q13.5 6.8 15 3z" /> : null} + {nfpLineInfo.drawLtlAt ? + : null} + {y ? this.drawDotAndLines(y, cycleDay.temperature.exclude, index) : null} ) @@ -196,7 +218,6 @@ function makeYAxis() { const tickPositions = [] const labels = [] - // for style reasons, we don't want the first and last tick for (let i = 1; i < numberOfTicks - 1; i++) { const y = tickDistance * i @@ -217,3 +238,51 @@ function makeYAxis() { return {labels, tickPositions} } + +function setUpFertilityStatusFunc() { + let cycleStatus + let cycleStartDate + let noMoreCycles = false + + function updateCurrentCycle(dateString) { + cycleStatus = getCycleStatusForDay(dateString) + if(!cycleStatus) { + noMoreCycles = true + return + } + if (cycleStatus.phases.preOvulatory) { + cycleStartDate = cycleStatus.phases.preOvulatory.start.date + } else { + cycleStartDate = cycleStatus.phases.periOvulatory.start.date + } + } + + function dateIsInPeriOrPostPhase(dateString) { + return ( + dateString >= cycleStatus.phases.periOvulatory.start.date && + dateString <= cycleStatus.phases.postOvulatory.start.date + ) + } + + return function(dateString) { + const ret = {} + if (!cycleStatus && !noMoreCycles) updateCurrentCycle(dateString) + if (noMoreCycles) return ret + + if (dateString < cycleStartDate) updateCurrentCycle(dateString) + if (noMoreCycles) return ret + + // now we know we have the current cycle + const tempShift = cycleStatus.temperatureShift + + if (tempShift && tempShift.firstHighMeasurementDay.date === dateString) { + ret.drawFhmLine = true + } + + if (tempShift && dateIsInPeriOrPostPhase(dateString)) { + ret.drawLtlAt = normalizeToScale(tempShift.ltl) + } + + return ret + } +} \ No newline at end of file diff --git a/lib/sympto-adapter.js b/lib/sympto-adapter.js index 337a1a7..1eb9e27 100644 --- a/lib/sympto-adapter.js +++ b/lib/sympto-adapter.js @@ -9,22 +9,8 @@ const { } = cycleModule() export function getFertilityStatusStringForDay(dateString) { - const cycle = getCycleForDay(dateString) - if (!cycle) return fertilityStatus.unknown - - const cycleInfo = {cycle: formatCycleForSympto(cycle)} - - const previousCycle = getPreviousCycle(dateString) - - if (previousCycle) { - cycleInfo.previousCycle = formatCycleForSympto(previousCycle) - const earlierCycles = getCyclesBefore(previousCycle[0]) - if (earlierCycles) { - cycleInfo.earlierCycles = earlierCycles.map(formatCycleForSympto) - } - } - - const status = getFertilityStatus(cycleInfo) + const status = getCycleStatusForDay(dateString) + if (!status) return fertilityStatus.unknown const phaseNameForDay = Object.keys(status.phases).find(phaseName => { const phase = status.phases[phaseName] @@ -41,6 +27,25 @@ export function getFertilityStatusStringForDay(dateString) { return mapToString(phaseNameForDay, dateString, status) } +export function getCycleStatusForDay(dateString) { + const cycle = getCycleForDay(dateString) + if (!cycle) return null + + const cycleInfo = {cycle: formatCycleForSympto(cycle)} + + const previousCycle = getPreviousCycle(dateString) + + if (previousCycle) { + cycleInfo.previousCycle = formatCycleForSympto(previousCycle) + const earlierCycles = getCyclesBefore(previousCycle[0]) + if (earlierCycles) { + cycleInfo.earlierCycles = earlierCycles.map(formatCycleForSympto) + } + } + + return getFertilityStatus(cycleInfo) +} + function mapToString(phaseNameForDay, dateString, status) { const mapping = { preOvulatory: () => fertilityStatus.infertile, From 4b5f4f905a5df29042cb365bb91319892eaa9541 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 19 Jul 2018 07:29:54 +0200 Subject: [PATCH 53/59] Extract styles and move vertical line a bit so it's completely visible --- components/chart/chart.js | 12 ++++++------ components/chart/styles.js | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/components/chart/chart.js b/components/chart/chart.js index 9030bfa..0710df8 100644 --- a/components/chart/chart.js +++ b/components/chart/chart.js @@ -72,14 +72,15 @@ export default class CycleChart extends Component { {nfpLineInfo.drawFhmLine ? : null} + {this.placeHorizontalGrid()} + {cycleDayNumber} {dateLabel} @@ -97,8 +98,7 @@ export default class CycleChart extends Component { y1={nfpLineInfo.drawLtlAt} x2={config.columnWidth} y2={nfpLineInfo.drawLtlAt} - stroke="orange" - strokeWidth="3" + {...styles.nfpLine} /> : null} {y ? this.drawDotAndLines(y, cycleDay.temperature.exclude, index) : null} diff --git a/components/chart/styles.js b/components/chart/styles.js index 9fa2633..2c101e6 100644 --- a/components/chart/styles.js +++ b/components/chart/styles.js @@ -62,6 +62,10 @@ const styles = { horizontalGrid: { stroke: 'lightgrey', strokeWidth: 1 + }, + nfpLine: { + stroke: '#00b159', + strokeWidth: 3 } } From b63a9560df5a2e6b94553f9b856e593e2e9248aa Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Thu, 19 Jul 2018 07:51:28 +0200 Subject: [PATCH 54/59] Draw horizontal line only during temp measuring phase --- components/chart/chart.js | 47 ++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/components/chart/chart.js b/components/chart/chart.js index 0710df8..5900164 100644 --- a/components/chart/chart.js +++ b/components/chart/chart.js @@ -65,7 +65,7 @@ export default class CycleChart extends Component { const label = styles.column.label const dateLabel = dateString.split('-').slice(1).join('-') const getFhmAndLtlInfo = setUpFertilityStatusFunc() - const nfpLineInfo = getFhmAndLtlInfo(dateString) + const nfpLineInfo = getFhmAndLtlInfo(dateString, cycleDay) return ( this.passDateToDayView(dateString)}> @@ -81,8 +81,12 @@ export default class CycleChart extends Component { {this.placeHorizontalGrid()} - {cycleDayNumber} - {dateLabel} + + {cycleDayNumber} + + + {dateLabel} + {cycleDay && cycleDay.bleeding ? : null} - {y ? this.drawDotAndLines(y, cycleDay.temperature.exclude, index) : null} + {y ? + this.drawDotAndLines(y, cycleDay.temperature.exclude, index) + : null + } ) } @@ -197,15 +204,18 @@ function makeColumnInfo(n) { function getPreviousDays(n) { const today = new Date() - today.setHours(0); today.setMinutes(0); today.setSeconds(0); today.setMilliseconds(0) + today.setHours(0) + today.setMinutes(0) + today.setSeconds(0) + today.setMilliseconds(0) const earlierDate = new Date(today - (range.DAY * n)) return range(earlierDate, today).reverse() } function normalizeToScale(temp) { - const temperatureScale = config.temperatureScale - const valueRelativeToScale = (temperatureScale.high - temp) / (temperatureScale.high - temperatureScale.low) + const scale = config.temperatureScale + const valueRelativeToScale = (scale.high - temp) / (scale.high - scale.low) const scaleHeight = config.chartHeight return scaleHeight * valueRelativeToScale } @@ -264,7 +274,15 @@ function setUpFertilityStatusFunc() { ) } - return function(dateString) { + function precededByAnotherTempValue(dateString) { + return Object.keys(cycleStatus.phases).some(phaseName => { + return cycleStatus.phases[phaseName].cycleDays.some(day => { + return day.temperature && day.date < dateString + }) + }) + } + + return function(dateString, cycleDay) { const ret = {} if (!cycleStatus && !noMoreCycles) updateCurrentCycle(dateString) if (noMoreCycles) return ret @@ -272,14 +290,21 @@ function setUpFertilityStatusFunc() { if (dateString < cycleStartDate) updateCurrentCycle(dateString) if (noMoreCycles) return ret - // now we know we have the current cycle const tempShift = cycleStatus.temperatureShift - if (tempShift && tempShift.firstHighMeasurementDay.date === dateString) { + if ( + tempShift && + tempShift.firstHighMeasurementDay.date === dateString + ) { ret.drawFhmLine = true } - if (tempShift && dateIsInPeriOrPostPhase(dateString)) { + if ( + tempShift && + cycleDay && + (cycleDay.temperature || precededByAnotherTempValue(dateString)) && + dateIsInPeriOrPostPhase(dateString) + ) { ret.drawLtlAt = normalizeToScale(tempShift.ltl) } From f52401d05db5f5f36e1149a7de3589d02fde28b4 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Mon, 23 Jul 2018 12:22:27 +0200 Subject: [PATCH 55/59] Add button to add dummy data --- components/home.js | 6 ++-- db/fixtures.js | 73 ++++++++++++++++++++++++++++++++++++++++++++ db.js => db/index.js | 35 ++++++++++++++++++--- 3 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 db/fixtures.js rename db.js => db/index.js (76%) diff --git a/components/home.js b/components/home.js index 6c3795d..212ac51 100644 --- a/components/home.js +++ b/components/home.js @@ -7,7 +7,7 @@ import { import { LocalDate } from 'js-joda' import styles from '../styles/index' import cycleModule from '../lib/cycle' -import { getOrCreateCycleDay, bleedingDaysSortedByDate, deleteAll } from '../db' +import { getOrCreateCycleDay, bleedingDaysSortedByDate, fillWithDummyData } from '../db' const getCycleDayNumber = cycleModule().getCycleDayNumber @@ -69,8 +69,8 @@ export default class Home extends Component { diff --git a/db/fixtures.js b/db/fixtures.js new file mode 100644 index 0000000..a9075f7 --- /dev/null +++ b/db/fixtures.js @@ -0,0 +1,73 @@ +function convertToSymptoFormat(val) { + const sympto = { date: val.date } + if (val.temperature) sympto.temperature = { value: val.temperature, exclude: false } + if (val.mucus) sympto.mucus = { + value: val.mucus, + exclude: false, + feeling: val.mucus, + texture: val.mucus + } + if (val.bleeding) sympto.bleeding = { value: val.bleeding, exclude: false } + return sympto +} + +export const cycleWithFhm = [ + { date: '2018-07-01', bleeding: 2 }, + { date: '2018-07-02', bleeding: 1 }, + { date: '2018-07-06', temperature: 36.2}, + { date: '2018-07-07', temperature: 36.35 }, + { date: '2018-07-09', temperature: 36.6 }, + { date: '2018-07-10', temperature: 36.45 }, + { date: '2018-07-12', temperature: 36.7, mucus: 0 }, + { date: '2018-07-13', temperature: 36.8, mucus: 4 }, + { date: '2018-07-15', temperature: 36.9, mucus: 2 }, + { date: '2018-07-16', temperature: 36.95, mucus: 2 }, + { date: '2018-07-17', temperature: 36.9, mucus: 2 }, + { date: '2018-07-18', temperature: 36.9, mucus: 2 } +].map(convertToSymptoFormat).reverse() + +export const longAndComplicatedCycle = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-06-02', temperature: 36.65 }, + { date: '2018-06-04', temperature: 36.6 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-09', temperature: 36.5, mucus: 4 }, + { date: '2018-06-10', temperature: 36.4, mucus: 2 }, + { date: '2018-06-13', temperature: 36.45, mucus: 3 }, + { date: '2018-06-14', temperature: 36.5, mucus: 4 }, + { date: '2018-06-15', temperature: 36.55, mucus: 4 }, + { date: '2018-06-16', temperature: 36.7, mucus: 3 }, + { date: '2018-06-17', temperature: 36.65, mucus: 3 }, + { date: '2018-06-18', temperature: 36.75, mucus: 4 }, + { date: '2018-06-19', temperature: 36.8, mucus: 1 }, + { date: '2018-06-20', temperature: 36.85, mucus: 2 }, + { date: '2018-06-21', temperature: 36.8, mucus: 2 }, + { date: '2018-06-22', temperature: 36.9, mucus: 2 }, + { date: '2018-06-25', temperature: 36.9, mucus: 1 }, + { date: '2018-06-26', temperature: 36.8, mucus: 1 }, + { date: '2018-06-27', temperature: 36.9, mucus: 1 } +].map(convertToSymptoFormat).reverse() + +export const cycleWithTempAndNoMucusShift = [ + { date: '2018-05-01', temperature: 36.6, bleeding: 2 }, + { date: '2018-05-02', temperature: 36.65 }, + { date: '2018-05-05', temperature: 36.55 }, + { date: '2018-05-06', temperature: 36.7, mucus: 0 }, + { date: '2018-05-08', temperature: 36.45, mucus: 1 }, + { date: '2018-05-09', temperature: 36.5, mucus: 4 }, + { date: '2018-05-10', temperature: 36.4, mucus: 2 }, + { date: '2018-05-11', temperature: 36.5, mucus: 3 }, + { date: '2018-05-13', temperature: 36.45, mucus: 3 }, + { date: '2018-05-14', temperature: 36.5, mucus: 4 }, + { date: '2018-05-15', temperature: 36.55, mucus: 4 }, + { date: '2018-05-16', temperature: 36.7, mucus: 3 }, + { date: '2018-05-17', temperature: 36.65, mucus: 3 }, + { date: '2018-05-18', temperature: 36.75, mucus: 4 }, + { date: '2018-05-19', temperature: 36.8, mucus: 4 }, + { date: '2018-05-20', temperature: 36.85, mucus: 4 }, + { date: '2018-05-23', temperature: 36.9, mucus: 3 }, + { date: '2018-05-24', temperature: 36.85, mucus: 4 }, + { date: '2018-05-26', temperature: 36.8, mucus: 4 }, + { date: '2018-05-27', temperature: 36.9, mucus: 4 } +].map(convertToSymptoFormat).reverse() \ No newline at end of file diff --git a/db.js b/db/index.js similarity index 76% rename from db.js rename to db/index.js index 7f5e3e5..b6971c7 100644 --- a/db.js +++ b/db/index.js @@ -1,6 +1,10 @@ import Realm from 'realm' import { LocalDate } from 'js-joda' - +import { + cycleWithTempAndNoMucusShift, + cycleWithFhm, + longAndComplicatedCycle +} from './fixtures' const TemperatureSchema = { name: 'Temperature', @@ -48,7 +52,7 @@ const CycleDaySchema = { } } -const db = new Realm({ +const realmConfig = { schema: [ CycleDaySchema, TemperatureSchema, @@ -57,7 +61,9 @@ const db = new Realm({ ], // we only want this in dev mode deleteRealmIfMigrationNeeded: true -}) +} + +const db = new Realm(realmConfig) const bleedingDaysSortedByDate = db.objects('CycleDay').filtered('bleeding != null').sorted('date', true) const temperatureDaysSortedByDate = db.objects('CycleDay').filtered('temperature != null').sorted('date', true) @@ -86,9 +92,28 @@ function getCycleDay(localDate) { return db.objectForPrimaryKey('CycleDay', localDate) } -function deleteAll() { +function fillWithDummyData() { + const dummyCycles = [ + cycleWithFhm, + longAndComplicatedCycle, + cycleWithTempAndNoMucusShift + ] + db.write(() => { db.deleteAll() + dummyCycles.forEach(cycle => { + cycle.forEach(day => { + const existing = getCycleDay(day.date) + if (existing) { + Object.keys(day).forEach(key => { + if (key === 'date') return + existing[key] = day[key] + }) + } else { + db.create('CycleDay', day) + } + }) + }) }) } @@ -108,7 +133,7 @@ export { bleedingDaysSortedByDate, temperatureDaysSortedByDate, cycleDaysSortedByDate, - deleteAll, + fillWithDummyData, getPreviousTemperature, getCycleDay } From 201a0d0ae1e24ebe446e6557610f7ff2305462e1 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Mon, 23 Jul 2018 12:59:40 +0200 Subject: [PATCH 56/59] Re-add delete all button --- components/chart/chart.js | 51 ++++++++++++++++++++++++--------------- components/home.js | 8 +++++- db/index.js | 7 ++++++ 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/components/chart/chart.js b/components/chart/chart.js index 5900164..ebb0d85 100644 --- a/components/chart/chart.js +++ b/components/chart/chart.js @@ -269,17 +269,33 @@ function setUpFertilityStatusFunc() { function dateIsInPeriOrPostPhase(dateString) { return ( - dateString >= cycleStatus.phases.periOvulatory.start.date && - dateString <= cycleStatus.phases.postOvulatory.start.date + dateString >= cycleStatus.phases.periOvulatory.start.date ) } function precededByAnotherTempValue(dateString) { - return Object.keys(cycleStatus.phases).some(phaseName => { - return cycleStatus.phases[phaseName].cycleDays.some(day => { - return day.temperature && day.date < dateString + return ( + // we are only interested in days that have a preceding + // temp + Object.keys(cycleStatus.phases).some(phaseName => { + return cycleStatus.phases[phaseName].cycleDays.some(day => { + return day.temperature && day.date < dateString + }) }) - }) + // and also a following temp, so we don't draw the line + // longer than necessary + && + cycleStatus.phases.postOvulatory.cycleDays.some(day => { + return day.temperature && day.date > dateString + }) + ) + } + + function isInTempMeasuringPhase(cycleDay, dateString) { + return ( + cycleDay && cycleDay.temperature + || precededByAnotherTempValue(dateString) + ) } return function(dateString, cycleDay) { @@ -292,20 +308,17 @@ function setUpFertilityStatusFunc() { const tempShift = cycleStatus.temperatureShift - if ( - tempShift && - tempShift.firstHighMeasurementDay.date === dateString - ) { - ret.drawFhmLine = true - } + if (tempShift) { + if (tempShift.firstHighMeasurementDay.date === dateString) { + ret.drawFhmLine = true + } - if ( - tempShift && - cycleDay && - (cycleDay.temperature || precededByAnotherTempValue(dateString)) && - dateIsInPeriOrPostPhase(dateString) - ) { - ret.drawLtlAt = normalizeToScale(tempShift.ltl) + if ( + dateIsInPeriOrPostPhase(dateString) && + isInTempMeasuringPhase(cycleDay, dateString) + ) { + ret.drawLtlAt = normalizeToScale(tempShift.ltl) + } } return ret diff --git a/components/home.js b/components/home.js index 212ac51..07b50db 100644 --- a/components/home.js +++ b/components/home.js @@ -7,7 +7,7 @@ import { import { LocalDate } from 'js-joda' import styles from '../styles/index' import cycleModule from '../lib/cycle' -import { getOrCreateCycleDay, bleedingDaysSortedByDate, fillWithDummyData } from '../db' +import { getOrCreateCycleDay, bleedingDaysSortedByDate, fillWithDummyData, deleteAll } from '../db' const getCycleDayNumber = cycleModule().getCycleDayNumber @@ -73,6 +73,12 @@ export default class Home extends Component { title="fill with example data"> + + + ) diff --git a/db/index.js b/db/index.js index b6971c7..948d805 100644 --- a/db/index.js +++ b/db/index.js @@ -117,6 +117,12 @@ function fillWithDummyData() { }) } +function deleteAll() { + db.write(() => { + db.deleteAll() + }) +} + function getPreviousTemperature(cycleDay) { cycleDay.wrappedDate = LocalDate.parse(cycleDay.date) const winner = temperatureDaysSortedByDate.find(day => { @@ -134,6 +140,7 @@ export { temperatureDaysSortedByDate, cycleDaysSortedByDate, fillWithDummyData, + deleteAll, getPreviousTemperature, getCycleDay } From 5ce6bc8cfc1e86ebe44d16cc5b561a0d4f6e1e94 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 24 Jul 2018 14:21:10 +0200 Subject: [PATCH 57/59] Handle cycle with mucus on first cycle day --- lib/sympto/index.js | 12 +++++++++--- lib/sympto/pre-ovulatory.js | 3 +++ test/sympto/fixtures.js | 23 +++++++++++++++++++++++ test/sympto/index.spec.js | 18 ++++++++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/lib/sympto/index.js b/lib/sympto/index.js index d032965..b930638 100644 --- a/lib/sympto/index.js +++ b/lib/sympto/index.js @@ -16,9 +16,15 @@ export default function getSymptoThermalStatus({ cycle, previousCycle, earlierCy if (previousCycle) { const statusForLast = getSymptoThermalStatus({ cycle: previousCycle }) if (statusForLast.temperatureShift) { - status.phases.preOvulatory = getPreOvulatoryPhase(cycle, [previousCycle, ...earlierCycles]) - if (status.phases.preOvulatory.cycleDays.length === cycle.length) { - return status + const preOvuPhase = getPreOvulatoryPhase( + cycle, + [previousCycle, ...earlierCycles] + ) + if (preOvuPhase) { + status.phases.preOvulatory = preOvuPhase + if (status.phases.preOvulatory.cycleDays.length === cycle.length) { + return status + } } } } diff --git a/lib/sympto/pre-ovulatory.js b/lib/sympto/pre-ovulatory.js index 01efb19..b1e7698 100644 --- a/lib/sympto/pre-ovulatory.js +++ b/lib/sympto/pre-ovulatory.js @@ -13,6 +13,9 @@ export default function(cycle, previousCycles) { return d.date <= preOvuEndDate }) const preOvulatoryDays = getDaysUntilFertileMucus(maybePreOvuDays) + // if mucus occurs on the 1st cycle day, there is no pre-ovu phase + if (!preOvulatoryDays.length) return null + let endDate if (preOvulatoryDays.length === maybePreOvuDays.length) { endDate = preOvuEndDate diff --git a/test/sympto/fixtures.js b/test/sympto/fixtures.js index 69792d5..6b61492 100644 --- a/test/sympto/fixtures.js +++ b/test/sympto/fixtures.js @@ -100,6 +100,29 @@ export const cycleWithEarlyMucus = [ { date: '2018-06-27', temperature: 36.9, mucus: 4 } ].map(convertToSymptoFormat) +export const cycleWithMucusOnFirstDay = [ + { date: '2018-06-01', temperature: 36.6, bleeding: 2, mucus: 3}, + { date: '2018-06-02', temperature: 36.65, mucus: 3 }, + { date: '2018-06-05', temperature: 36.55 }, + { date: '2018-06-06', temperature: 36.7, mucus: 0 }, + { date: '2018-06-08', temperature: 36.45, mucus: 1 }, + { date: '2018-06-09', temperature: 36.5, mucus: 4 }, + { date: '2018-06-10', temperature: 36.4, mucus: 2 }, + { date: '2018-06-11', temperature: 36.5, mucus: 3 }, + { date: '2018-06-13', temperature: 36.45, mucus: 3 }, + { date: '2018-06-14', temperature: 36.5, mucus: 4 }, + { date: '2018-06-15', temperature: 36.55, mucus: 4 }, + { date: '2018-06-16', temperature: 36.7, mucus: 3 }, + { date: '2018-06-17', temperature: 36.65, mucus: 3 }, + { date: '2018-06-18', temperature: 36.75, mucus: 4 }, + { date: '2018-06-19', temperature: 36.8, mucus: 4 }, + { date: '2018-06-20', temperature: 36.85, mucus: 4 }, + { date: '2018-06-23', temperature: 36.9, mucus: 3 }, + { date: '2018-06-24', temperature: 36.85, mucus: 4 }, + { date: '2018-06-26', temperature: 36.8, mucus: 4 }, + { date: '2018-06-27', temperature: 36.9, mucus: 4 } +].map(convertToSymptoFormat) + export const cycleWithoutAnyShifts = [ { date: '2018-06-01', temperature: 36.6, bleeding: 2 }, { date: '2018-06-02', temperature: 36.65 }, diff --git a/test/sympto/index.spec.js b/test/sympto/index.spec.js index aed8f3a..7f65319 100644 --- a/test/sympto/index.spec.js +++ b/test/sympto/index.spec.js @@ -9,6 +9,7 @@ import { cycleWithoutAnyShifts, fiveDayCycle, cycleWithEarlyMucus, + cycleWithMucusOnFirstDay, mucusPeakAndFhmOnSameDay, fhmTwoDaysBeforeMucusPeak, fhm5DaysAfterMucusPeak, @@ -453,6 +454,23 @@ describe('sympto', () => { }) }) }) + + it('shortens the pre-ovu phase if mucus occurs even on the first day', () => { + const status = getSensiplanStatus({ + cycle: cycleWithMucusOnFirstDay, + previousCycle: fhmOnDay12, + earlierCycles: Array(10).fill(fhmOnDay12) + }) + + + expect(Object.keys(status.phases).length).to.eql(1) + + expect(status.phases.periOvulatory).to.eql({ + start: { date: '2018-06-01' }, + cycleDays: cycleWithMucusOnFirstDay + }) + }) + it('lengthens the pre-ovu phase if >= 12 cycles with fhm > 13', () => { const status = getSensiplanStatus({ cycle: longAndComplicatedCycle, From a2400983cf6d75bcb86423503d64bd343f2e3dd3 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 24 Jul 2018 14:30:20 +0200 Subject: [PATCH 58/59] Move status labels to cycle day labels --- components/cycle-day/labels/labels.js | 29 ++++++++++++--------------- labels/labels.js | 8 -------- lib/sympto-adapter.js | 2 +- 3 files changed, 14 insertions(+), 25 deletions(-) delete mode 100644 labels/labels.js diff --git a/components/cycle-day/labels/labels.js b/components/cycle-day/labels/labels.js index 25909df..6e294ef 100644 --- a/components/cycle-day/labels/labels.js +++ b/components/cycle-day/labels/labels.js @@ -1,17 +1,14 @@ -const bleeding = ['spotting', 'light', 'medium', 'heavy'] -const mucusFeeling = ['dry', 'nothing', 'wet', 'slippery'] -const mucusTexture = ['nothing', 'creamy', 'egg white'] -const mucusNFP = ['t', 'Ø', 'f', 'S', '+S'] -const cervixOpening = ['closed', 'medium', 'open'] -const cervixFirmness = ['hard', 'soft'] -const cervixPosition = ['low', 'medium', 'high'] +export const bleeding = ['spotting', 'light', 'medium', 'heavy'] +export const mucusFeeling = ['dry', 'nothing', 'wet', 'slippery'] +export const mucusTexture = ['nothing', 'creamy', 'egg white'] +export const mucusNFP = ['t', 'Ø', 'f', 'S', '+S'] +export const cervixOpening = ['closed', 'medium', 'open'] +export const cervixFirmness = ['hard', 'soft'] +export const cervixPosition = ['low', 'medium', 'high'] -export { - bleeding, - mucusFeeling, - mucusTexture, - mucusNFP, - cervixOpening, - cervixFirmness, - cervixPosition -} +export const fertilityStatus = { + fertile: 'fertile', + infertile: 'infertile', + fertileUntilEvening: 'Fertile phase ends in the evening', + unknown: 'We cannot show any cycle information because no menses has been entered' +} \ No newline at end of file diff --git a/labels/labels.js b/labels/labels.js deleted file mode 100644 index 20c8ad0..0000000 --- a/labels/labels.js +++ /dev/null @@ -1,8 +0,0 @@ -export const bleeding = ['spotting', 'light', 'medium', 'heavy'] - -export const fertilityStatus = { - fertile: 'fertile', - infertile: 'infertile', - fertileUntilEvening: 'Fertile phase ends in the evening', - unknown: 'We cannot show any cycle information because no menses has been entered' -} \ No newline at end of file diff --git a/lib/sympto-adapter.js b/lib/sympto-adapter.js index 1eb9e27..b6ab217 100644 --- a/lib/sympto-adapter.js +++ b/lib/sympto-adapter.js @@ -1,6 +1,6 @@ import getFertilityStatus from './sympto' import cycleModule from './cycle' -import { fertilityStatus } from '../labels/labels' +import { fertilityStatus } from '../components/cycle-day/labels/labels' const { getCycleForDay, From e3b8e5d71782698b66aa844ca02b120a84deeac6 Mon Sep 17 00:00:00 2001 From: Julia Friesel Date: Tue, 24 Jul 2018 17:52:44 +0200 Subject: [PATCH 59/59] Fix label, replace old property --- components/cycle-day/cycle-day-overview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cycle-day/cycle-day-overview.js b/components/cycle-day/cycle-day-overview.js index 526fe82..66be0c1 100644 --- a/components/cycle-day/cycle-day-overview.js +++ b/components/cycle-day/cycle-day-overview.js @@ -72,7 +72,7 @@ export default class DayView extends Component { if (this.cycleDay.mucus) { const mucus = this.cycleDay.mucus if (typeof mucus.feeling === 'number' && typeof mucus.texture === 'number') { - mucusLabel = `${feelingLabels[mucus.feeling]} + ${textureLabels[mucus.texture]} ( ${computeSensiplanMucusLabels[mucus.computedNfp]} )` + mucusLabel = `${feelingLabels[mucus.feeling]} + ${textureLabels[mucus.texture]} ( ${computeSensiplanMucusLabels[mucus.value]} )` if (mucus.exclude) mucusLabel = "( " + mucusLabel + " )" } } else {