diff --git a/.eslintrc b/.eslintrc index 24f9ab56bf502942f377681466e45ee1cac72bbc..6ec6205a75c3c3f5668108ae62979aa3fafedbd7 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": [1, {"ignoreStrings": true}] } } \ No newline at end of file diff --git a/components/chart/chart.js b/components/chart/chart.js index 32983c1c8003de4fc7a11fe234ab29c294d06df8..30ed5d7b471a3384a80aa5b28d5058f6213b8704 100644 --- a/components/chart/chart.js +++ b/components/chart/chart.js @@ -11,11 +11,12 @@ 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' +import { getCycleStatusForDay } from '../../lib/sympto-adapter' -const getCycleDayNumber = getCycleDayNumberModule() +const getCycleDayNumber = cycleModule().getCycleDayNumber const yAxis = makeYAxis(config) @@ -63,13 +64,29 @@ 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, cycleDay) return ( <G onPress={() => this.passDateToDayView(dateString)}> <Rect {...styles.column.rect} /> + {nfpLineInfo.drawFhmLine ? + <Line + x1={0 + styles.nfpLine.strokeWidth / 2} + y1="20" + x2={0 + styles.nfpLine.strokeWidth / 2} + y2={config.chartHeight - 20} + {...styles.nfpLine} + /> : null} + {this.placeHorizontalGrid()} - <Text {...label.number} y={config.cycleDayNumberRowY}>{cycleDayNumber}</Text> - <Text {...label.date} y={config.dateRowY}>{dateLabel}</Text> + + <Text {...label.number} y={config.cycleDayNumberRowY}> + {cycleDayNumber} + </Text> + <Text {...label.date} y={config.dateRowY}> + {dateLabel} + </Text> {cycleDay && cycleDay.bleeding ? <Path {...styles.bleedingIcon} @@ -79,10 +96,23 @@ export default class CycleChart extends Component { Q13.5 6.8 15 3z" /> : null} + {nfpLineInfo.drawLtlAt ? + <Line + x1="0" + y1={nfpLineInfo.drawLtlAt} + x2={config.columnWidth} + y2={nfpLineInfo.drawLtlAt} + {...styles.nfpLine} + /> : null} + + {y ? + this.drawDotAndLines(y, cycleDay.temperature.exclude, index) + : null + } {cycleDay && cycleDay.mucus ? <Circle {...styles.mucusIcon} - fill={styles.mucusIconShades[cycleDay.mucus.computedNfp]} + fill={styles.mucusIconShades[cycleDay.mucus.value]} /> : null} {y ? this.drawDotAndLines(y, cycleDay.temperature.exclude, index) : null} @@ -181,15 +211,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 } @@ -202,7 +235,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 @@ -223,3 +255,79 @@ 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 + ) + } + + function precededByAnotherTempValue(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) { + const ret = {} + if (!cycleStatus && !noMoreCycles) updateCurrentCycle(dateString) + if (noMoreCycles) return ret + + if (dateString < cycleStartDate) updateCurrentCycle(dateString) + if (noMoreCycles) return ret + + const tempShift = cycleStatus.temperatureShift + + if (tempShift) { + if (tempShift.firstHighMeasurementDay.date === dateString) { + ret.drawFhmLine = true + } + + if ( + dateIsInPeriOrPostPhase(dateString) && + isInTempMeasuringPhase(cycleDay, dateString) + ) { + ret.drawLtlAt = normalizeToScale(tempShift.ltl) + } + } + + return ret + } +} \ No newline at end of file diff --git a/components/chart/styles.js b/components/chart/styles.js index 4e06b73203cb7216cf57d899195b132e4d1ec49c..8c2ab2e40c719382a99ab14f7bf73cc2badd99ed 100644 --- a/components/chart/styles.js +++ b/components/chart/styles.js @@ -74,6 +74,10 @@ const styles = { horizontalGrid: { stroke: 'lightgrey', strokeWidth: 1 + }, + nfpLine: { + stroke: '#00b159', + strokeWidth: 3 } } diff --git a/components/cycle-day/cycle-day-overview.js b/components/cycle-day/cycle-day-overview.js index c2730d14771149cd56e8c72218880bbdea7895f4..094bc654b81978b09ca73b382db066ddf75769cf 100644 --- a/components/cycle-day/cycle-day-overview.js +++ b/components/cycle-day/cycle-day-overview.js @@ -14,10 +14,10 @@ import { cervixFirmness as firmnessLabels, cervixPosition as positionLabels } from './labels/labels' -import cycleDayModule from '../../lib/get-cycle-day-number' +import cycleDayModule from '../../lib/cycle' import { bleedingDaysSortedByDate } from '../../db' -const getCycleDayNumber = cycleDayModule() +const getCycleDayNumber = cycleDayModule().getCycleDayNumber export default class DayView extends Component { constructor(props) { @@ -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 { diff --git a/components/cycle-day/index.js b/components/cycle-day/index.js index 3d2ffde8bb8e007723499c84d4be538c2506e3ef..e0eb666c4c1b96f35624b19ab32c2922babbb236 100644 --- a/components/cycle-day/index.js +++ b/components/cycle-day/index.js @@ -4,7 +4,8 @@ import { Text, ScrollView } from 'react-native' -import cycleDayModule from '../../lib/get-cycle-day-number' +import cycleModule from '../../lib/cycle' +import { getFertilityStatusStringForDay } from '../../lib/sympto-adapter' import DayView from './cycle-day-overview' import BleedingEditView from './symptoms/bleeding' import TemperatureEditView from './symptoms/temperature' @@ -14,7 +15,7 @@ import CervixEditView from './symptoms/cervix' import styles from '../../styles' import actionButtonModule from './action-buttons' -const getCycleDayNumber = cycleDayModule() +const getCycleDayNumber = cycleModule().getCycleDayNumber export default class Day extends Component { constructor(props) { @@ -34,6 +35,7 @@ export default class Day extends Component { render() { const cycleDayNumber = getCycleDayNumber(this.cycleDay.date) + const fertilityStatus = getFertilityStatusStringForDay(this.cycleDay.date) return ( <ScrollView> <View style={ styles.cycleDayDateView }> @@ -42,7 +44,14 @@ export default class Day extends Component { </Text> </View > <View style={ styles.cycleDayNumberView }> - { cycleDayNumber && <Text style={styles.cycleDayNumber} >Cycle day {cycleDayNumber}</Text> } + { cycleDayNumber && + <Text style={styles.cycleDayNumber} > + Cycle day {cycleDayNumber} + </Text> } + + <Text style={styles.cycleDayNumber} > + {fertilityStatus} + </Text> </View > <View> { diff --git a/components/cycle-day/labels/labels.js b/components/cycle-day/labels/labels.js index 25909dfcd8ecc5d4bccbdb49f6f195006bda38ac..6e294ef8408ca22f8b65f1bae51465033ceeda5d 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/components/cycle-day/symptoms/mucus.js b/components/cycle-day/symptoms/mucus.js index 4a090d2f0297e6d78ffa6d405041267e54b90e48..abafe5668255fa81da5c1467e62e334820fbd1a2 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.feeling, texture: this.state.texture, - computedNfp: computeSensiplanValue(this.state.feeling, this.state.texture), + value: computeSensiplanValue(this.state.feeling, this.state.texture), exclude: this.state.exclude }) }, diff --git a/components/home.js b/components/home.js index fa62ad7763e0400d32823fd912b8bdc93a8ebe59..48af5f5d72b19b64727f492a04e78e2ff5d3e86d 100644 --- a/components/home.js +++ b/components/home.js @@ -6,11 +6,11 @@ import { ScrollView } from 'react-native' import { LocalDate } from 'js-joda' -import styles from '../styles' -import cycleDayModule from '../lib/get-cycle-day-number' -import { getOrCreateCycleDay, bleedingDaysSortedByDate, deleteAll } from '../db' +import styles from '../styles/index' +import cycleModule from '../lib/cycle' +import { getOrCreateCycleDay, bleedingDaysSortedByDate, fillWithDummyData, deleteAll } from '../db' -const getCycleDayNumber = cycleDayModule() +const getCycleDayNumber = cycleModule().getCycleDayNumber export default class Home extends Component { constructor(props) { @@ -68,6 +68,12 @@ export default class Home extends Component { title="Go to chart"> </Button> </View> + <View style={styles.homeButton}> + <Button + onPress={() => fillWithDummyData()} + title="fill with example data"> + </Button> + </View> <View style={styles.homeButton}> <Button onPress={() => deleteAll()} diff --git a/db/fixtures.js b/db/fixtures.js new file mode 100644 index 0000000000000000000000000000000000000000..a9075f78ba0510d1bc0dd4fa89f88c952d87543b --- /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 77% rename from db.js rename to db/index.js index 6fb743d0b21038def678efcfb2647f71a4fa229e..ed3e27567848dfe54957eac6bb50e20cb849db85 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', @@ -27,7 +31,7 @@ const MucusSchema = { properties: { feeling: 'int', texture: 'int', - computedNfp: 'int', + value: 'int', exclude: 'bool' } } @@ -66,7 +70,7 @@ const CycleDaySchema = { } } -const db = new Realm({ +const realmConfig = { schema: [ CycleDaySchema, TemperatureSchema, @@ -76,7 +80,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) @@ -105,6 +111,31 @@ function getCycleDay(localDate) { return db.objectForPrimaryKey('CycleDay', localDate) } +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) + } + }) + }) + }) +} + function deleteAll() { db.write(() => { db.deleteAll() @@ -127,6 +158,7 @@ export { bleedingDaysSortedByDate, temperatureDaysSortedByDate, cycleDaysSortedByDate, + fillWithDummyData, deleteAll, getPreviousTemperature, getCycleDay diff --git a/lib/cycle.js b/lib/cycle.js new file mode 100644 index 0000000000000000000000000000000000000000..f60a74f321d040e9937f7726d995ba24cbdec651 --- /dev/null +++ b/lib/cycle.js @@ -0,0 +1,129 @@ +import * as joda from 'js-joda' +const LocalDate = joda.LocalDate + + +export default function config(opts) { + let bleedingDaysSortedByDate + let cycleDaysSortedByDate + let maxBreakInBleeding + + if (!opts) { + // we only want to require (and run) the db module + // when not running the tests + bleedingDaysSortedByDate = require('../db').bleedingDaysSortedByDate + cycleDaysSortedByDate = require('../db').cycleDaysSortedByDate + maxBreakInBleeding = 1 + } else { + bleedingDaysSortedByDate = opts.bleedingDaysSortedByDate || [] + cycleDaysSortedByDate = opts.cycleDaysSortedByDate || [] + 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) { + withWrappedDates.forEach(day => delete day.wrappedDate) + return null + } + + const previousBleedingDays = withWrappedDates.slice(firstBleedingDayBeforeTargetDayIndex) + + 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 }) => { + return wrappedDate.equals(periodThreshold) || wrappedDate.isAfter(periodThreshold) + }) + } + + withWrappedDates.forEach(day => delete day.wrappedDate) + 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) { + const lastMensesStart = getLastMensesStart(targetDateString) + if (!lastMensesStart) return null + 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 + } + + function getCyclesBefore(targetCycleStartDay) { + return collectPreviousCycles([], targetCycleStartDay.date) + } + + function collectPreviousCycles(acc, startOfFollowingCycle) { + const cycle = getPreviousCycle(startOfFollowingCycle) + if (!cycle || !cycle.length) return acc + acc.push(cycle) + return collectPreviousCycles(acc, cycle[cycle.length - 1].date) + } + + function getPreviousCycle(dateString) { + const startOfCycle = getLastMensesStart(dateString) + if (!startOfCycle) return null + const dateBeforeStartOfCycle = LocalDate.parse(startOfCycle.date).minusDays(1).toString() + return getCycleForDay(dateBeforeStartOfCycle) + } + + function getCycleForDay(dayOrDate) { + const dateString = typeof dayOrDate === 'string' ? dayOrDate : dayOrDate.date + const cycleStart = 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 { + getCycleDayNumber, + getCycleForDay, + getPreviousCycle, + getCyclesBefore + } +} \ No newline at end of file diff --git a/lib/get-cycle-day-number.js b/lib/get-cycle-day-number.js deleted file mode 100644 index d1b056a33e94b7c08e7b4f7e50a3ea70ba9e73d4..0000000000000000000000000000000000000000 --- a/lib/get-cycle-day-number.js +++ /dev/null @@ -1,48 +0,0 @@ -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 - - 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) - - // 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/sympto-adapter.js b/lib/sympto-adapter.js new file mode 100644 index 0000000000000000000000000000000000000000..b6ab2171789b4e8a2cb8ea5123ff623192c071cc --- /dev/null +++ b/lib/sympto-adapter.js @@ -0,0 +1,69 @@ +import getFertilityStatus from './sympto' +import cycleModule from './cycle' +import { fertilityStatus } from '../components/cycle-day/labels/labels' + +const { + getCycleForDay, + getCyclesBefore, + getPreviousCycle +} = cycleModule() + +export function getFertilityStatusStringForDay(dateString) { + const status = getCycleStatusForDay(dateString) + if (!status) return fertilityStatus.unknown + + 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) +} + +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, + 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) { + // we get earliest last, but sympto wants earliest first + cycle.reverse() + return cycle +} \ No newline at end of file diff --git a/lib/sympto/index.js b/lib/sympto/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b9306380b639e9978966612a2ea08a887ca52d00 --- /dev/null +++ b/lib/sympto/index.js @@ -0,0 +1,102 @@ +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 getSymptoThermalStatus({ cycle, previousCycle, earlierCycles = [] }) { + throwIfArgsAreNotInRequiredFormat([cycle, ...earlierCycles]) + + const status = { + phases: {} + } + + // if there was no first higher measurement in the previous cycle, + // no infertile pre-ovulatory phase may be assumed + if (previousCycle) { + const statusForLast = getSymptoThermalStatus({ cycle: previousCycle }) + if (statusForLast.temperatureShift) { + const preOvuPhase = getPreOvulatoryPhase( + cycle, + [previousCycle, ...earlierCycles] + ) + if (preOvuPhase) { + status.phases.preOvulatory = preOvuPhase + if (status.phases.preOvulatory.cycleDays.length === cycle.length) { + return status + } + } + } + } + + status.phases.periOvulatory = { + start: { date: null }, + cycleDays: [] + } + const periPhase = status.phases.periOvulatory + + if (status.phases.preOvulatory) { + const prePhase = status.phases.preOvulatory + 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 { + periPhase.start.date = cycle[0].date + periPhase.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 + + let periOvulatoryEnd + const tempOver = temperatureShift.evaluationCompleteDay.date + const mucusOver = mucusShift.evaluationCompleteDay.date + + 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: previousPeriDays.slice(previousPeriEndIndex) + } + + periPhase.cycleDays = previousPeriDays.slice(0, previousPeriEndIndex + 1) + periPhase.end = status.phases.postOvulatory.start + + status.mucusShift = mucusShift + status.temperatureShift = temperatureShift + + return status +} + +function throwIfArgsAreNotInRequiredFormat(cycles) { + cycles.forEach(cycle => { + assert.ok(Array.isArray(cycle)) + 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 => { + 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') + 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/minus-8-day-rule.js b/lib/sympto/minus-8-day-rule.js new file mode 100644 index 0000000000000000000000000000000000000000..2964b373b518fcbe5030d38558d601e73ce3cbe8 --- /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/mucus.js b/lib/sympto/mucus.js new file mode 100644 index 0000000000000000000000000000000000000000..9eb37efb6d5af600a24a05d91c373492bc44521b --- /dev/null +++ b/lib/sympto/mucus.js @@ -0,0 +1,40 @@ +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)) + + for (let i = 0; i < mucusDays.length; i++) { + const day = mucusDays[i] + if (day.mucus.value !== bestQuality) continue + + // 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 bestQualityOccursIn3FollowingDays = threeFollowingDays.some(day => { + return day.mucus.value >= bestQuality + }) + if (bestQualityOccursIn3FollowingDays) continue + + const cycleDayIndex = cycleDays.indexOf(day) + const relevantDays = cycleDays + .slice(cycleDayIndex + 1, tempEvalEndIndex + 1) + .filter(day => day.mucus && !day.mucus.exclude) + + const noBestQualityUntilEndOfTempEval = relevantDays.every(day => { + return day.mucus.value < bestQuality + }) + + if (noBestQualityUntilEndOfTempEval) { + return { + detected: true, + mucusPeak: day, + evaluationCompleteDay: threeFollowingDays[threeFollowingDays.length - 1] + } + } + } + + return { detected: false } +} + diff --git a/lib/sympto/pre-ovulatory.js b/lib/sympto/pre-ovulatory.js new file mode 100644 index 0000000000000000000000000000000000000000..b1e76981c105b536912e3ef3de2cd7e74a2de145 --- /dev/null +++ b/lib/sympto/pre-ovulatory.js @@ -0,0 +1,46 @@ +import { LocalDate } from "js-joda" +import apply8DayRule from './minus-8-day-rule' + +export default function(cycle, previousCycles) { + let preOvuPhaseLength = 5 + + const minus8DayRuleResult = apply8DayRule(previousCycles) + if (minus8DayRuleResult) preOvuPhaseLength = minus8DayRuleResult + + const startDate = LocalDate.parse(cycle[0].date) + const preOvuEndDate = startDate.plusDays(preOvuPhaseLength - 1).toString() + const maybePreOvuDays = cycle.slice(0, preOvuPhaseLength).filter(d => { + 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 + } else { + endDate = preOvulatoryDays[preOvulatoryDays.length - 1].date + } + + return { + cycleDays: preOvulatoryDays, + start: { + date: preOvulatoryDays[0].date + }, + end: { + date: endDate + } + } +} + +function getDaysUntilFertileMucus(days) { + const firstFertileMucusDayIndex = days.findIndex(day => { + return 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/lib/sympto/temperature.js b/lib/sympto/temperature.js new file mode 100644 index 0000000000000000000000000000000000000000..387cd6378457fda7d98cd633ee3e9a0eb02f88fa --- /dev/null +++ b/lib/sympto/temperature.js @@ -0,0 +1,106 @@ +export default function (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 daysBefore = temperatureDays.slice(0, i).slice(-6) + const temps = daysBefore.map(day => day.temp) + return Math.max(...temps) + } + + 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) continue + + // is the temp a candidate for a first high measurement? + const ltl = getLtl(i) + const temp = temperatureDays[i].temp + if (temp <= ltl) continue + + const shift = checkIfFirstHighMeasurement(temp, i, temperatureDays, ltl) + + if (shift.detected) { + shift.firstHighMeasurementDay = temperatureDays[i].originalCycleDay + return shift + } + } + + return { detected: false } +} + +function checkIfFirstHighMeasurement(temp, i, temperatureDays, ltl) { + // need at least 3 high temps to form a high temperature level + if (i > temperatureDays.length - 3) { + return { detected: false } + } + const nextDays = temperatureDays.slice(i + 1, i + 4) + + 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 { + detected: true, + rule: 0, + ltl, + evaluationCompleteDay: thirdDay.originalCycleDay + } +} + +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 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(nextDays, ltl) { + const secondIsLow = nextDays[0].temp <= ltl + const thirdIsLow = nextDays[1].temp <= ltl + if ((secondIsLow || thirdIsLow) && !(secondIsLow && thirdIsLow)) { + return true + } else { + return false + } +} + +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/package-lock.json b/package-lock.json index 763d5ec73af71059fb4ad3e7348e8689a1eab6d4..995d84f65d39c9638643437c3f5f7b03bb8754b4 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", @@ -8169,6 +8177,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 1849fda2b4b2891fd1aa14169437562a041f788c..10fa3665ca240bc3d9c6be489b16d2f5c537ff60 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,10 @@ "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", "date-range": "0.0.2", "js-joda": "^1.8.2", "moment": "^2.22.1", diff --git a/styles/index.js b/styles/index.js index 52fc65d4a18bbcb3a3584afa4ca6069365df00ec..1b09eb4c37ec79d41f94ab2a9a4e117ec1f9eb6a 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 diff --git a/test/cycle.spec.js b/test/cycle.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..3b82c40d271b6cb1d37fc25fdfdef2a7a35c4f03 --- /dev/null +++ b/test/cycle.spec.js @@ -0,0 +1,336 @@ +import chai from 'chai' +import dirtyChai from 'dirty-chai' +import cycleModule from '../lib/cycle' + +const expect = chai.expect +chai.use(dirtyChai) + +describe('getCycleDay', () => { + it('works for a simple example', () => { + const bleedingDays = [{ + date: '2018-05-10', + bleeding: { + value: 2 + } + }, { + date: '2018-05-09', + bleeding: { + value: 2 + } + }, { + date: '2018-05-03', + bleeding: { + value: 2 + } + }] + const getCycleDayNumber = cycleModule({ bleedingDaysSortedByDate: bleedingDays }).getCycleDayNumber + const targetDate = '2018-05-17' + const result = getCycleDayNumber(targetDate) + expect(result).to.eql(9) + }) + + it('works if some bleedings are exluded', function () { + const bleedingDays = [{ + date: '2018-05-10', + bleeding: { + value: 2, + exclude: true + } + }, { + date: '2018-05-09', + bleeding: { + value: 2, + exclude: true + } + }, { + date: '2018-05-03', + bleeding: { + value: 2 + } + }] + const targetDate = '2018-05-17' + const getCycleDayNumber = cycleModule({ bleedingDaysSortedByDate: bleedingDays }).getCycleDayNumber + const result = getCycleDayNumber(targetDate) + expect(result).to.eql(15) + }) + + it('gets the correct number if the target day is not in the current cycle', () => { + const bleedingDays = [{ + date: '2018-05-13', + bleeding: { + value: 2 + } + }, { + date: '2018-04-11', + bleeding: { + value: 2 + } + }, { + date: '2018-04-10', + bleeding: { + value: 2 + } + }] + + const targetDate = '2018-04-27' + const getCycleDayNumber = cycleModule({ bleedingDaysSortedByDate: bleedingDays }).getCycleDayNumber + const result = getCycleDayNumber(targetDate) + expect(result).to.eql(18) + }) + + it('gets the correct number if the target day is the only bleeding day', () => { + const bleedingDays = [{ + date: '2018-05-13', + bleeding: { + value: 2 + } + }] + + const targetDate = '2018-05-13' + 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 with cycle thresholds', () => { + const maxBreakInBleeding = 3 + + it('disregards bleeding breaks shorter than max allowed bleeding break in a bleeding period', () => { + const bleedingDays = [{ + date: '2018-05-14', + bleeding: { + value: 2 + } + }, { + date: '2018-05-10', + bleeding: { + value: 2 + } + }] + + const targetDate = '2018-05-17' + const getCycleDayNumber = cycleModule({ bleedingDaysSortedByDate: bleedingDays, maxBreakInBleeding }).getCycleDayNumber + const result = getCycleDayNumber(targetDate) + expect(result).to.eql(8) + }) + + it('counts bleeding breaks longer than maxAllowedBleedingBreak in a bleeding period', () => { + const bleedingDays = [{ + date: '2018-05-14', + bleeding: { + value: 2 + } + }, { + date: '2018-05-09', + bleeding: { + value: 2 + } + }] + const targetDate = '2018-05-17' + const getCycleDayNumber = cycleModule({ bleedingDaysSortedByDate: bleedingDays, maxBreakInBleeding }).getCycleDayNumber + const result = getCycleDayNumber(targetDate) + expect(result).to.eql(4) + }) + }) +}) + +describe('getCyclesBefore', () => { + 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 } + }, + ] + + const { getCyclesBefore } = cycleModule({ + cycleDaysSortedByDate, + bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding) + }) + const result = getCyclesBefore(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 } + }, + ] + ]) + }) +}) + +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 diff --git a/test/get-cycle-day.spec.js b/test/get-cycle-day.spec.js deleted file mode 100644 index 8aaadddf7f7ec310a3c3e9ad91673a7fb024c016..0000000000000000000000000000000000000000 --- a/test/get-cycle-day.spec.js +++ /dev/null @@ -1,146 +0,0 @@ -import chai from 'chai' -import dirtyChai from 'dirty-chai' - -const expect = chai.expect -chai.use(dirtyChai) - -import getCycleDayNumberModule from '../lib/get-cycle-day-number' - -describe('getCycleDay', () => { - it('works for a simple example', function () { - const bleedingDays = [{ - date: '2018-05-10', - bleeding: { - value: 2 - } - }, { - date: '2018-05-09', - bleeding: { - value: 2 - } - }, { - date: '2018-05-03', - bleeding: { - value: 2 - } - }] - const getCycleDayNumber = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays}) - const targetDate = '2018-05-17' - const result = getCycleDayNumber(targetDate) - expect(result).to.eql(9) - }) - - it('works if some bleedings are exluded', function () { - const bleedingDays = [{ - date: '2018-05-10', - bleeding: { - value: 2, - exclude: true - } - }, { - date: '2018-05-09', - bleeding: { - value: 2, - exclude: true - } - }, { - date: '2018-05-03', - bleeding: { - value: 2 - } - }] - const targetDate = '2018-05-17' - const getCycleDayNumber = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays}) - const result = getCycleDayNumber(targetDate) - expect(result).to.eql(15) - }) - - it('gets the correct number if the target day is not in the current cycle', () => { - const bleedingDays = [{ - date: '2018-05-13', - bleeding: { - value: 2 - } - }, { - date: '2018-04-11', - bleeding: { - value: 2 - } - }, { - date: '2018-04-10', - bleeding: { - value: 2 - } - }] - - const targetDate = '2018-04-27' - const getCycleDayNumber = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays}) - const result = getCycleDayNumber(targetDate) - expect(result).to.eql(18) - }) - - it('gets the correct number if the target day is the only bleeding day', () => { - const bleedingDays = [{ - date: '2018-05-13', - bleeding: { - value: 2 - } - }] - - const targetDate = '2018-05-13' - const getCycleDayNumber = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays}) - 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 = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays}) - 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 = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays, maxBreakInBleeding }) - 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 = getCycleDayNumberModule({bleedingDaysSortedByDate: bleedingDays, maxBreakInBleeding }) - const result = getCycleDayNumber(targetDate) - expect(result).to.eql(4) - }) -}) \ No newline at end of file diff --git a/test/sensiplan-mucus.spec.js b/test/sensiplan-mucus.spec.js index feb4e93df4b441dceaeed0a2e1505a7830bd183a..df1c461e8b9cbec78fac968399a63cc38c555a19 100644 --- a/test/sensiplan-mucus.spec.js +++ b/test/sensiplan-mucus.spec.js @@ -6,7 +6,7 @@ chai.use(dirtyChai) import getSensiplanMucus from '../lib/sensiplan-mucus' -describe.only('getSensiplanMucus', () => { +describe('getSensiplanMucus', () => { describe('results in t for:', () => { it('dry feeling and no texture', function () { diff --git a/test/sympto/fixtures.js b/test/sympto/fixtures.js new file mode 100644 index 0000000000000000000000000000000000000000..6b61492812c28970ff89c1a7cc0971acd3f85412 --- /dev/null +++ b/test/sympto/fixtures.js @@ -0,0 +1,301 @@ + +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 } + return sympto +} + +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 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 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) + +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 }, + { 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 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) + +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 }, + { 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) + +export const fiveDayCycle = [ + { date: '2018-06-01', bleeding: 2 }, + { date: '2018-06-03', bleeding: 3 }, +].map(convertToSymptoFormat) + +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 }, + { 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) + +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 }, + { 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) + +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 }, + { 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) + +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 }, + { 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) + +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 }, + { 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 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 }, + { 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) + +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 new file mode 100644 index 0000000000000000000000000000000000000000..7f653191d39d7c3d43aeda2c14a30c0a5a1c74a1 --- /dev/null +++ b/test/sympto/index.spec.js @@ -0,0 +1,625 @@ +import chai from 'chai' +import getSensiplanStatus from '../../lib/sympto' +import { AssertionError } from 'assert' +import { + cycleWithoutFhm, + longAndComplicatedCycle, + cycleWithTempAndNoMucusShift, + cycleWithFhm, + cycleWithoutAnyShifts, + fiveDayCycle, + cycleWithEarlyMucus, + cycleWithMucusOnFirstDay, + mucusPeakAndFhmOnSameDay, + fhmTwoDaysBeforeMucusPeak, + fhm5DaysAfterMucusPeak, + mucusPeak5DaysAfterFhm, + mucusPeakTwoDaysBeforeFhm, + fhmOnDay12, + fhmOnDay15, + mucusPeakSlightlyBeforeTempShift +} from './fixtures' + +const expect = chai.expect + +describe('sympto', () => { + describe('with no previous higher measurement', () => { + it('with no shifts detects only peri-ovulatory', function () { + const status = getSensiplanStatus({ + cycle: cycleWithoutAnyShifts, + previousCycle: cycleWithoutFhm + }) + + expect(status).to.eql({ + + phases: { + periOvulatory: { + start: { date: '2018-06-01' }, + cycleDays: cycleWithoutAnyShifts + } + }, + }) + }) + + it('with shifts detects only peri-ovulatory and post-ovulatory', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycle: cycleWithoutFhm + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + + 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}) => 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') + }) + }) + }) + 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: cycleWithFhm + }) + + expect(Object.keys(status.phases).length).to.eql(1) + + 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', () => { + it('according to 5-day-rule', function () { + const status = getSensiplanStatus({ + cycle: cycleWithTempAndNoMucusShift, + previousCycle: cycleWithFhm + }) + + expect(Object.keys(status.phases).length).to.eql(2) + + expect(status.phases.preOvulatory).to.eql({ + 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'), + start: { date: '2018-06-06' } + }) + }) + it('according to 5-day-rule with shortened pre-phase', function () { + const status = getSensiplanStatus({ + cycle: cycleWithEarlyMucus, + previousCycle: cycleWithFhm + }) + + expect(Object.keys(status.phases).length).to.eql(2) + + 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 () { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycle: cycleWithFhm + }) + + expect(Object.keys(status.phases).length).to.eql(3) + + expect(status.phases.preOvulatory).to.eql({ + 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: 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: longAndComplicatedCycle + .filter(({date}) => date >= '2018-06-21'), + start: { date: '2018-06-21', time: '18:00'} + }) + }) + + }) + }) + + describe('combining first higher measurment and mucus peak', () => { + it('with fhM + mucus peak on same day finds start of postovu phase', () => { + const status = getSensiplanStatus({ + cycle: mucusPeakAndFhmOnSameDay, + previousCycle: cycleWithFhm + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + + 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: cycleWithFhm + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + + 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('another example for mucus peak before temp shift', () => { + const status = getSensiplanStatus({ + cycle: mucusPeakSlightlyBeforeTempShift, + previousCycle: cycleWithFhm + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + + 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, + previousCycle: cycleWithFhm + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + + 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: cycleWithFhm + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + + 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: cycleWithFhm + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + + 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('applying the minus-8 rule', () => { + it('shortens the pre-ovu phase if there is a previous <13 fhm', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycle: fhmOnDay15, + earlierCycles: [fhmOnDay12, ...Array(10).fill(fhmOnDay15)] + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + + 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 pre-ovu phase with prev <13 fhm even with <12 cycles', () => { + const status = getSensiplanStatus({ + cycle: longAndComplicatedCycle, + previousCycle: fhmOnDay12, + earlierCycles: Array(10).fill(fhmOnDay12) + }) + + expect(status.temperatureShift).to.be.an('object') + expect(status.mucusShift).to.be.an('object') + + 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, + previousCycle: fhmOnDay12, + earlierCycles: Array(10).fill(fhmOnDay12) + }) + + + 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('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, + previousCycle: fhmOnDay15, + earlierCycles: Array(11).fill(fhmOnDay15) + }) + + + 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, + previousCycle: fhmOnDay15, + earlierCycles: Array(10).fill(fhmOnDay15) + }) + + + 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, + previousCycle: cycleWithoutFhm, + earlierCycles: [...Array(12).fill(fhmOnDay15)] + }) + + + 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') + }) + }) + }) + + describe('when args are wrong', () => { + it('throws when arg object is not in right format', () => { + const wrongObject = { hello: 'world' } + expect(() => getSensiplanStatus(wrongObject)).to.throw(AssertionError) + }) + it('throws if cycle array is empty', () => { + expect(() => getSensiplanStatus({cycle: []})).to.throw(AssertionError) + }) + it('throws if cycle days are not in right format', () => { + expect(() => getSensiplanStatus({ + cycle: [{ + hello: 'world', + bleeding: { value: 0 } + }], + earlierCycles: [[{ + date: '1992-09-09', + bleeding: { value: 0 } + }]] + })).to.throw(AssertionError) + expect(() => getSensiplanStatus({ + cycle: [{ + date: '2018-04-13', + temperature: {value: '35'}, + bleeding: { value: 0 } + }], + earlierCycles: [[{ + date: '1992-09-09', + bleeding: { value: 0 } + }]] + })).to.throw(AssertionError) + expect(() => getSensiplanStatus({ + cycle: [{ + date: '09-14-2017', + bleeding: { value: 0 } + }], + earlierCycles: [[{ + date: '1992-09-09', + bleeding: { value: 0 } + }]] + })).to.throw(AssertionError) + }) + it('throws if first cycle day does not have bleeding value', () => { + expect(() => getSensiplanStatus({ + cycle: [{ + date: '2017-01-01', + bleeding: { + value: 'medium' + } + }], + earlierCycles: [[ + { + date: '2017-09-23', + } + ]] + })).to.throw(AssertionError) + }) + }) +}) \ No newline at end of file diff --git a/test/sympto/mucus.spec.js b/test/sympto/mucus.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f8b83d2f8aabb6ba1a9ced2e7e0796943a6efae0 --- /dev/null +++ b/test/sympto/mucus.spec.js @@ -0,0 +1,53 @@ +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, 12) + expect(status).to.eql({ + detected: true, + mucusPeak: { + date: 10, + mucus: { value: 3 } + }, + evaluationCompleteDay: { + date: 13, + mucus: { value: 0 } + } + }) + }) + + 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, 30) + 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, 30) + expect(status).to.eql({ detected: false }) + }) + }) + }) +}) \ No newline at end of file diff --git a/test/sympto/temperature.spec.js b/test/sympto/temperature.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..ce7e6c1689a749c315d797d61cb6e26fcc755c2d --- /dev/null +++ b/test/sympto/temperature.spec.js @@ -0,0 +1,252 @@ +import chai from 'chai' +import getTemperatureStatus from '../../lib/sympto/temperature' + +const expect = chai.expect + +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', () => { + 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', () => { + 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({ + 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', () => { + 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', () => { + 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', () => { + 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', () => { + 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({ + ltl: 36.6, + firstHighMeasurementDay: { + date: 10, + temperature: { value: 36.7 } + }, + evaluationCompleteDay: { + date: 12, + temperature: { value: 36.9 } + }, + detected: true, + rule: 0 + }) + }) + + 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) + expect(status).to.eql({ detected: false }) + }) + }) + + describe('1st exception rule', () => { + 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({ + ltl: 36.6, + firstHighMeasurementDay: { + date: 7, + temperature: { value: 36.8 } + }, + + evaluationCompleteDay: { + date: 10, + temperature : { value: 36.63 } + }, + detected: true, + rule: 1 + }) + }) + + 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', () => { + 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', () => { + 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({ + ltl: 36.6, + 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', () => { + 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({ + ltl: 36.6, + firstHighMeasurementDay: { + date: 7, + temperature: { value: 36.8 } + }, + + evaluationCompleteDay: { + date: 10, + temperature : { value: 36.8 } + }, + detected: true, + rule: 2 + }) + }) + + 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({ + ltl: 36.6, + firstHighMeasurementDay: { + date: 7, + temperature: { value: 36.8 } + }, + + evaluationCompleteDay: { + date: 10, + temperature : { value: 36.8 } + }, + detected: true, + rule: 2 + }) + }) + + + 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', () => { + 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', () => { + 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({ + ltl: 36.85, + firstHighMeasurementDay: { + date: 11, + temperature: { value: 36.9 } + }, + + evaluationCompleteDay: { + date: 14, + temperature : { value: 37.04 } + }, + detected: true, + rule: 2 + }) + }) + + }) + }) +}) \ No newline at end of file