Merge branch '268-sympto-respect-max-cycle-length' into 'master'

Resolve "sympto: respect max cycle length"

Closes #268

See merge request bloodyhealth/drip!142
This commit is contained in:
Julia Friesel
2019-01-05 09:50:43 +00:00
3 changed files with 278 additions and 27 deletions
+2 -5
View File
@@ -9,17 +9,14 @@ import cycleModule from '../lib/cycle'
import {getCycleLengthStats as getCycleInfo} from '../lib/cycle-length' import {getCycleLengthStats as getCycleInfo} from '../lib/cycle-length'
import {stats as labels} from '../i18n/en/labels' import {stats as labels} from '../i18n/en/labels'
import AppText from './app-text' import AppText from './app-text'
import { getCycleStartsSortedByDate } from '../db'
export default class Stats extends Component { export default class Stats extends Component {
render() { render() {
const allMensesStarts = getCycleStartsSortedByDate() const cycleLengths = cycleModule().getAllCycleLengths()
const atLeastOneCycle = allMensesStarts.length > 1 const atLeastOneCycle = cycleLengths.length > 1
let cycleLengths
let numberOfCycles let numberOfCycles
let cycleInfo let cycleInfo
if (atLeastOneCycle) { if (atLeastOneCycle) {
cycleLengths = cycleModule().getAllCycleLengths()
numberOfCycles = cycleLengths.length numberOfCycles = cycleLengths.length
if (numberOfCycles > 1) { if (numberOfCycles > 1) {
cycleInfo = getCycleInfo(cycleLengths) cycleInfo = getCycleInfo(cycleLengths)
+26 -20
View File
@@ -1,5 +1,5 @@
import * as joda from 'js-joda' import * as joda from 'js-joda'
import {getCycleLengthStats} from './cycle-length' import { getCycleLengthStats } from './cycle-length'
const LocalDate = joda.LocalDate const LocalDate = joda.LocalDate
const DAYS = joda.ChronoUnit.DAYS const DAYS = joda.ChronoUnit.DAYS
@@ -40,7 +40,7 @@ export default function config(opts) {
const lastMensesLocalDate = LocalDate.parse(lastMensesStart.date) const lastMensesLocalDate = LocalDate.parse(lastMensesStart.date)
const diffInDays = lastMensesLocalDate.until(targetDate, DAYS) const diffInDays = lastMensesLocalDate.until(targetDate, DAYS)
// take maxCycleLength into account (we don't display cycle day numbers higher than 99 at the moment) // take maxCycleLength into account (we don't display cycle day numbers higher than 99 at the moment)
if (diffInDays >= 99) return null if (diffInDays >= maxCycleLength) return null
// cycle starts at day 1 // cycle starts at day 1
return diffInDays + 1 return diffInDays + 1
} }
@@ -61,28 +61,38 @@ export default function config(opts) {
if (startFromHere < 0) return null if (startFromHere < 0) return null
return cycleStartsSortedByDate return cycleStartsSortedByDate
.slice(startFromHere) .slice(startFromHere)
.map(getCycleForCycleStartDay) .map(start => getCycleForCycleStartDay(start))
// filter the ones exceeding maxCycleLength, those are null
.filter(cycle => cycle)
} }
function getCycleForCycleStartDay(startDay) { function getCycleForCycleStartDay(startDay, todayDate) {
const todayAsLocalDate = todayDate ? LocalDate.parse(todayDate) : LocalDate.now()
const cycleStartIndex = cycleDaysSortedByDate.indexOf(startDay) const cycleStartIndex = cycleDaysSortedByDate.indexOf(startDay)
const i = cycleStartsSortedByDate.indexOf(startDay) const i = cycleStartsSortedByDate.indexOf(startDay)
const startLocalDate = LocalDate.parse(startDay.date)
const nextMensesStart = cycleStartsSortedByDate[i - 1] const nextMensesStart = cycleStartsSortedByDate[i - 1]
let cycle
let cycleLength
if (nextMensesStart) { if (nextMensesStart) {
return cycleDaysSortedByDate.slice( cycle = cycleDaysSortedByDate.slice(
cycleDaysSortedByDate.indexOf(nextMensesStart) + 1, cycleDaysSortedByDate.indexOf(nextMensesStart) + 1,
cycleStartIndex + 1, cycleStartIndex + 1,
) )
const nextLocalDate = LocalDate.parse(nextMensesStart.date)
cycleLength = startLocalDate.until(nextLocalDate, DAYS)
} else { } else {
return cycleDaysSortedByDate.slice(0, cycleStartIndex + 1) cycle = cycleDaysSortedByDate.slice(0, cycleStartIndex + 1)
cycleLength = startLocalDate.until(todayAsLocalDate, DAYS)
} }
return cycleLength > maxCycleLength ? null : cycle
} }
function getCycleForDay(dayOrDate) { function getCycleForDay(dayOrDate, todayDate) {
const dateString = typeof dayOrDate === 'string' ? dayOrDate : dayOrDate.date const dateString = typeof dayOrDate === 'string' ? dayOrDate : dayOrDate.date
const cycleStart = getLastMensesStartForDay(dateString) const cycleStart = getLastMensesStartForDay(dateString)
if (!cycleStart) return null if (!cycleStart) return null
return getCycleForCycleStartDay(cycleStart) return getCycleForCycleStartDay(cycleStart, todayDate)
} }
function isMensesStart(cycleDay) { function isMensesStart(cycleDay) {
@@ -138,24 +148,19 @@ export default function config(opts) {
function getAllCycleLengths() { function getAllCycleLengths() {
return cycleStartsSortedByDate return cycleStartsSortedByDate
.map(day => LocalDate.parse(day.date)) .map(day => LocalDate.parse(day.date))
.reduce((lengths, cycleStart, i, startsAsLocalDates) => { .map((cycleStart, i, startsAsLocalDates) => {
if (i === startsAsLocalDates.length - 1) return lengths if (i === cycleStartsSortedByDate.length - 1) return null
const prevCycleStart = startsAsLocalDates[i + 1] const prevCycleStart = startsAsLocalDates[i + 1]
const cycleLength = prevCycleStart.until(cycleStart, DAYS) return prevCycleStart.until(cycleStart, DAYS)
if (cycleLength <= maxCycleLength) { lengths.push(cycleLength) } })
return lengths .filter(length => length && length <= maxCycleLength)
}, [])
} }
function getPredictedMenses() { function getPredictedMenses() {
const allMensesStarts = cycleStartsSortedByDate const cycleLengths = getAllCycleLengths()
const atLeastOneCycle = allMensesStarts.length > 1 if (cycleLengths.length < minCyclesForPrediction) {
if (!atLeastOneCycle ||
allMensesStarts.length < minCyclesForPrediction
) {
return [] return []
} }
const cycleLengths = getAllCycleLengths()
const cycleInfo = getCycleLengthStats(cycleLengths) const cycleInfo = getCycleLengthStats(cycleLengths)
const periodDistance = Math.round(cycleInfo.mean) const periodDistance = Math.round(cycleInfo.mean)
let periodStartVariation let periodStartVariation
@@ -169,6 +174,7 @@ export default function config(opts) {
if (periodDistance - 5 < periodStartVariation) { // otherwise predictions overlap if (periodDistance - 5 < periodStartVariation) { // otherwise predictions overlap
return [] return []
} }
const allMensesStarts = cycleStartsSortedByDate
let lastStart = LocalDate.parse(allMensesStarts[0].date) let lastStart = LocalDate.parse(allMensesStarts[0].date)
const predictedMenses = [] const predictedMenses = []
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
+250 -2
View File
@@ -72,7 +72,26 @@ describe('getCycleDayNumber', () => {
expect(result).to.be.null() expect(result).to.be.null()
}) })
it('returns null if the cycle is longer than the max', function () {
const cycleStarts = [{
date: '2018-05-09',
isCycleStart: true,
bleeding: {
value: 2
}
}, {
date: '2018-05-03',
isCycleStart: true,
bleeding: { value: 2 }
}]
// we use the default 99 days max length
const getCycleDayNumber = cycleModule({
cycleStartsSortedByDate: cycleStarts
}).getCycleDayNumber
const targetDate = '2018-08-16'
const result = getCycleDayNumber(targetDate)
expect(result).to.be.null()
})
}) })
describe('getPreviousCycle', () => { describe('getPreviousCycle', () => {
@@ -246,6 +265,64 @@ describe('getPreviousCycle', () => {
const result = getPreviousCycle('2018-04-18') const result = getPreviousCycle('2018-04-18')
expect(result).to.eql(null) expect(result).to.eql(null)
}) })
it('returns null when the previous cycle > maxcyclelength', () => {
const cycleDaysSortedByDate = [
{
date: '2018-07-05',
bleeding: { value: 2 }
},
{
date: '2018-06-05',
bleeding: { value: 2 }
},
{
date: '2018-05-05',
mucus: { value: 2 }
},
{
date: '2018-05-04',
bleeding: { value: 2 }
},
{
date: '2018-05-03',
bleeding: { value: 2 }
},
{
date: '2018-04-05',
mucus: { value: 2 }
},
{
date: '2018-04-04',
mucus: { value: 2 }
},
{
date: '2018-04-03',
mucus: { value: 2 }
},
{
date: '2018-04-02',
bleeding: { value: 2 }
},
]
const cycleStarts = [
'2018-07-05',
'2018-06-05',
'2018-05-03',
'2018-04-02'
]
const { getPreviousCycle } = cycleModule({
cycleDaysSortedByDate,
cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => {
return cycleStarts.includes(d.date)
}),
maxCycleLength: 2
})
const result = getPreviousCycle('2018-06-08')
expect(result).to.eql(null)
})
}) })
describe('getCyclesBefore', () => { describe('getCyclesBefore', () => {
@@ -343,6 +420,68 @@ describe('getCyclesBefore', () => {
] ]
]) ])
}) })
it('skips cycles that are longer than max', () => {
const cycleDaysSortedByDate = [
{
date: '2018-07-05',
bleeding: { value: 2 }
},
{
date: '2018-06-05',
bleeding: { value: 2 }
},
{
date: '2018-05-05',
mucus: { value: 2 }
},
{
date: '2018-05-04',
bleeding: { value: 2 }
},
{
date: '2018-05-03',
bleeding: { value: 2 }
},
{
date: '2018-04-05',
mucus: { value: 2 }
},
{
date: '2018-04-04',
mucus: { value: 2 }
},
{
date: '2018-04-03',
mucus: { value: 2 }
},
{
date: '2018-04-02',
bleeding: { value: 2 }
},
]
const cycleStarts = [
'2018-07-05',
'2018-06-05',
'2018-05-03',
'2018-04-02'
]
const { getCyclesBefore } = cycleModule({
cycleDaysSortedByDate,
cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => {
return cycleStarts.includes(d.date)
}),
maxCycleLength: 30
})
const result = getCyclesBefore(cycleDaysSortedByDate[0])
expect(result.length).to.eql(1)
expect(result).to.eql([[{
bleeding: { value: 2 },
date: "2018-06-05"
}]])
})
}) })
describe('getCycleForDay', () => { describe('getCycleForDay', () => {
@@ -399,7 +538,7 @@ describe('getCycleForDay', () => {
}) })
it('gets cycle that has only one day', () => { it('gets cycle that has only one day', () => {
const result = getCycleForDay('2018-07-05') const result = getCycleForDay('2018-07-05', '2018-08-01')
expect(result.length).to.eql(1) expect(result.length).to.eql(1)
expect(result).to.eql([ expect(result).to.eql([
{ {
@@ -433,6 +572,18 @@ describe('getCycleForDay', () => {
expect(result).to.eql(null) expect(result).to.eql(null)
}) })
it('returns null if the cycle is longer than the max', () => {
const { getCycleForDay } = cycleModule({
cycleDaysSortedByDate,
cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => {
return cycleStarts.includes(d.date)
}),
maxCycleLength: 3
})
const result = getCycleForDay('2018-04-04')
expect(result).to.eql(null)
})
it('gets cycle for day', () => { it('gets cycle for day', () => {
const result = getCycleForDay('2018-04-04') const result = getCycleForDay('2018-04-04')
expect(result.length).to.eql(4) expect(result.length).to.eql(4)
@@ -525,6 +676,47 @@ describe('getPredictedMenses', () => {
const result = getPredictedMenses() const result = getPredictedMenses()
expect(result).to.eql([]) expect(result).to.eql([])
}) })
it('if number of cycles is below minCyclesForPrediction because one of them is too long', () => {
const cycleDaysSortedByDate = [
{
date: '2018-06-02',
bleeding: { value: 2 }
},
{
date: '2018-06-01',
bleeding: { value: 2 }
},
{
date: '2018-05-01',
bleeding: { value: 2 }
},
{
date: '2018-04-03',
bleeding: { value: 2 }
},
{
date: '2018-04-02',
bleeding: { value: 2 }
},
{
date: '2018-04-01',
bleeding: { value: 2 }
},
]
const cycleStarts = ['2018-06-01', '2018-05-01', '2018-04-01']
const { getPredictedMenses } = cycleModule({
cycleDaysSortedByDate,
bleedingDaysSortedByDate: cycleDaysSortedByDate.filter(d => d.bleeding),
cycleStartsSortedByDate: cycleDaysSortedByDate.filter(d => {
return cycleStarts.includes(d.date)
}),
maxCycleLength: 2
})
const result = getPredictedMenses()
expect(result).to.eql([])
})
}) })
describe('works', () => { describe('works', () => {
it('for one completed cycle with minCyclesForPrediction = 1', () => { it('for one completed cycle with minCyclesForPrediction = 1', () => {
@@ -714,6 +906,62 @@ describe('getPredictedMenses', () => {
] ]
expect(result).to.eql(expectedResult) expect(result).to.eql(expectedResult)
}) })
it('does not count cycles longer than max', () => {
const cycleDaysSortedByDate = [
{
date: '2018-08-01',
bleeding: { value: 2 }
},
{
date: '2018-07-14',
bleeding: { value: 2 }
},
{
date: '2018-07-04',
bleeding: { value: 2 }
},
{
date: '2018-06-20',
bleeding: { value: 2 }
},
{
date: '2018-04-20',
bleeding: { value: 2 }
},
]
const { getPredictedMenses } = cycleModule({
cycleDaysSortedByDate,
cycleStartsSortedByDate: cycleDaysSortedByDate,
maxCycleLength: 50
})
const result = getPredictedMenses()
const expectedResult = [
[
'2018-08-13',
'2018-08-14',
'2018-08-15',
'2018-08-16',
'2018-08-17',
],
[
'2018-08-27',
'2018-08-28',
'2018-08-29',
'2018-08-30',
'2018-08-31',
],
[
'2018-09-10',
'2018-09-11',
'2018-09-12',
'2018-09-13',
'2018-09-14',
]
]
expect(result).to.eql(expectedResult)
})
}) })
}) })