diff --git a/bleeding.js b/bleeding.js index 01fc4ce78b942c2ceb28642883adfe7d534845c3..31dc8c69756e98fa1feef6891ed26847539dd353 100644 --- a/bleeding.js +++ b/bleeding.js @@ -10,7 +10,9 @@ import styles from './styles' import { saveBleeding } from './db' import { formatDateForViewHeader } from './format' import { bleeding as labels } from './labels' -import getCycleDay from './get-cycle-day' +import cycleDayModule from './get-cycle-day-number' + +const getCycleDayNumber = cycleDayModule() export default class Bleeding extends Component { constructor(props) { @@ -39,7 +41,7 @@ export default class Bleeding extends Component { return ( <View style={styles.container}> <Text style={styles.welcome}>{formatDateForViewHeader(day.date)}</Text> - <Text>Cycle day {getCycleDay()}</Text> + <Text>Cycle day {getCycleDayNumber(day.date)}</Text> <Text>Bleeding</Text> <RadioForm radio_props={bleedingRadioProps} diff --git a/calendar.js b/calendar.js index 9294ac099e965f000734794c2dd48bfd35b6d9a1..b23025d3545a82e50a4cd29e533b760e082fbbd7 100644 --- a/calendar.js +++ b/calendar.js @@ -13,7 +13,7 @@ export default class DatePickView extends Component { } componentWillUnmount() { - bleedingDaysSortedByDate.removeListener(setStateWithCalendarFormattedDays) + bleedingDaysSortedByDate.removeAllListeners() } passDateToDayView(result) { diff --git a/day-view.js b/day-view.js index 0d4e45cb8d386eb40264a36cb052e622f75c3ccf..037521931e65b78649cf2440286b6166c93b7dc7 100644 --- a/day-view.js +++ b/day-view.js @@ -7,19 +7,28 @@ import { import styles from './styles' import { formatDateForViewHeader } from './format' import { bleeding as labels} from './labels' -import getCycleDay from './get-cycle-day' +import cycleDayModule from './get-cycle-day-number' +import { bleedingDaysSortedByDate } from './db' + +const getCycleDayNumber = cycleDayModule() export default class DayView extends Component { constructor(props) { super(props) + this.cycleDay = props.navigation.state.params.cycleDay this.state = { - cycleDay: props.navigation.state.params.cycleDay + cycleDayNumber: getCycleDayNumber(this.cycleDay.date), } + bleedingDaysSortedByDate.addListener(setStateWithCurrentCycleDayNumber.bind(this)) + } + + componentWillUnmount() { + bleedingDaysSortedByDate.removeAllListeners() } render() { const navigate = this.props.navigation.navigate - const cycleDay = this.state.cycleDay + const cycleDay = this.cycleDay const bleedingValue = cycleDay.bleeding && cycleDay.bleeding.value let bleedingLabel if (typeof bleedingValue === 'number') { @@ -40,7 +49,7 @@ export default class DayView extends Component { return ( <View style={styles.container}> <Text style={styles.welcome}>{formatDateForViewHeader(cycleDay.date)}</Text> - <Text>Cycle day {getCycleDay()}</Text> + <Text>Cycle day {getCycleDayNumber(cycleDay.date)}</Text> <Text style={styles.welcome}>{bleedingLabel}</Text> <Text style={styles.welcome}>{temperatureLabel}</Text> <Button @@ -54,4 +63,10 @@ export default class DayView extends Component { </View > ) } +} + +function setStateWithCurrentCycleDayNumber() { + this.setState({ + cycleDayNumber: getCycleDayNumber(this.cycleDay.date) + }) } \ No newline at end of file diff --git a/db.js b/db.js index 8f34199cb1cffb5f6c505be25c3ffafc6094b286..a469530553835f226e4266aec0348fbb282d4e13 100644 --- a/db.js +++ b/db.js @@ -1,10 +1,6 @@ -import realm from 'realm' +import Realm from 'realm' import { LocalDate } from 'js-joda' -let db -let cycleDaysSortedbyDate = [] -let bleedingDaysSortedByDate = [] -let temperatureDaysSortedByDate const TemperatureSchema = { name: 'Temperature', @@ -38,21 +34,18 @@ const CycleDaySchema = { } } -async function openDatabase() { - db = await realm.open({ - schema: [ - CycleDaySchema, - TemperatureSchema, - BleedingSchema - ], - // we only want this in dev mode - deleteRealmIfMigrationNeeded: true - }) +const db = new Realm({ + schema: [ + CycleDaySchema, + TemperatureSchema, + BleedingSchema + ], + // we only want this in dev mode + deleteRealmIfMigrationNeeded: true +}) - cycleDaysSortedbyDate = db.objects('CycleDay').sorted('date', true) - bleedingDaysSortedByDate = db.objects('CycleDay').filtered('bleeding != null').sorted('date', true) - temperatureDaysSortedByDate = db.objects('CycleDay').filtered('temperature != null').sorted('date', true) -} +const bleedingDaysSortedByDate = db.objects('CycleDay').filtered('bleeding != null').sorted('date', true) +const temperatureDaysSortedByDate = db.objects('CycleDay').filtered('temperature != null').sorted('date', true) function saveTemperature(cycleDay, temperature) { db.write(() => { @@ -60,6 +53,8 @@ function saveTemperature(cycleDay, temperature) { }) } +const getCycleDaysSortedByDateView = () => db.objects('CycleDay').sorted('date', true) + function saveBleeding(cycleDay, bleeding) { db.write(() => { cycleDay.bleeding = bleeding @@ -78,6 +73,12 @@ function getOrCreateCycleDay(localDate) { return result } +function deleteAll() { + db.write(() => { + db.deleteAll() + }) +} + function getPreviousTemperature(cycleDay) { cycleDay.wrappedDate = LocalDate.parse(cycleDay.date) const winner = temperatureDaysSortedByDate.find(day => { @@ -89,11 +90,11 @@ function getPreviousTemperature(cycleDay) { } export { - cycleDaysSortedbyDate, - openDatabase, saveTemperature, saveBleeding, getOrCreateCycleDay, bleedingDaysSortedByDate, + getCycleDaysSortedByDateView, + deleteAll, getPreviousTemperature } diff --git a/get-cycle-day-number.js b/get-cycle-day-number.js new file mode 100644 index 0000000000000000000000000000000000000000..90646e46acc02d44bc4db5709a58df5aa934b3ee --- /dev/null +++ b/get-cycle-day-number.js @@ -0,0 +1,48 @@ +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/get-cycle-day.js b/get-cycle-day.js deleted file mode 100644 index 4930e24ef0663c9fd164f4523d291d7df11dcac0..0000000000000000000000000000000000000000 --- a/get-cycle-day.js +++ /dev/null @@ -1 +0,0 @@ -export default () => 6 \ No newline at end of file diff --git a/home.js b/home.js index c02055e6f9876a4370443cfd6f28e18f3713265c..9ae59176a70ae1e4b44caa4d4898308c0891dc27 100644 --- a/home.js +++ b/home.js @@ -6,12 +6,26 @@ import { } from 'react-native' import { LocalDate } from 'js-joda' import styles from './styles' -import getCycleDay from './get-cycle-day' -import { getOrCreateCycleDay } from './db' +import cycleDayModule from './get-cycle-day-number' +import { getOrCreateCycleDay, bleedingDaysSortedByDate, deleteAll } from './db' + +const getCycleDayNumber = cycleDayModule() export default class Home extends Component { constructor(props) { super(props) + this.todayDateString = LocalDate.now().toString() + const cycleDayNumber = getCycleDayNumber(this.todayDateString) + + this.state = { + welcomeText: determineWelcomeText(cycleDayNumber) + } + + bleedingDaysSortedByDate.addListener(setStateWithCurrentWelcomeText.bind(this)) + } + + componentWillUnmount() { + bleedingDaysSortedByDate.removeAllListeners() } passTodayToDayView() { @@ -25,7 +39,7 @@ export default class Home extends Component { const navigate = this.props.navigation.navigate return ( <View style={styles.container}> - <Text style={styles.welcome}>Welcome! Today is day {getCycleDay()} of your current cycle</Text> + <Text style={styles.welcome}>{this.state.welcomeText}</Text> <Button onPress={() => this.passTodayToDayView()} title="Edit symptoms for today"> @@ -34,7 +48,21 @@ export default class Home extends Component { onPress={() => navigate('calendar')} title="Go to calendar"> </Button> + <Button + onPress={() => deleteAll()} + title="delete everything"> + </Button> </View> ) } } + +function determineWelcomeText(cycleDayNumber) { + const welcomeTextWithCycleDay = `Welcome! Today is day ${cycleDayNumber} of your current cycle` + const welcomeText = `Welcome! We don't have enough information to know what your current cycle day is` + return cycleDayNumber ? welcomeTextWithCycleDay : welcomeText +} + +function setStateWithCurrentWelcomeText() { + this.setState({ welcomeText: determineWelcomeText(getCycleDayNumber(this.todayDateString)) }) +} diff --git a/index.js b/index.js index f1543b7f59a076d1539af1d4567760115840d236..408a2f0a4f5c4eca1c780a50f05b467785c97608 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,4 @@ import { AppRegistry } from 'react-native' -import Home from './app' -import { openDatabase } from './db' +import App from './app' -// TODO error handling -openDatabase() - .then(() => { - AppRegistry.registerComponent('home', () => Home) - }) \ No newline at end of file +AppRegistry.registerComponent('home', () => App) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 615ce54dd05f5890170a089e44d6d40713d19a22..4ec7dc76bc631480f8574c0f4d9ea2c2f2d02351 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2766,6 +2766,12 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "dirty-chai": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/dirty-chai/-/dirty-chai-2.0.1.tgz", + "integrity": "sha512-ys79pWKvDMowIDEPC6Fig8d5THiC0DJ2gmTeGzVAoEH18J8OzLud0Jh7I9IWg3NSk8x2UocznUuFmfHCXYZx9w==", + "dev": true + }, "doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", diff --git a/package.json b/package.json index 98d3dae7b46ea994138777d56c6ee4c9f84ed9d1..ff64b52e0e0accea0b3fb9ce537356b24681353d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "devDependencies": { "babel-preset-react-native": "4.0.0", "chai": "^4.1.2", + "dirty-chai": "^2.0.1", "eslint": "^4.19.1", "eslint-plugin-react": "^7.8.2", "mocha": "^5.2.0", diff --git a/temperature.js b/temperature.js index ba7121c5d23a7e5f2491897faed0d08c263076e3..5e2f00a94c09bb6ff435e0e3aeb691b91fdf9db6 100644 --- a/temperature.js +++ b/temperature.js @@ -10,7 +10,9 @@ import { import styles from './styles' import { saveTemperature, getPreviousTemperature } from './db' import { formatDateForViewHeader } from './format' -import getCycleDay from './get-cycle-day' +import cycleDayModule from './get-cycle-day-number' + +const getCycleDayNumber = cycleDayModule() export default class Temp extends Component { constructor(props) { @@ -37,7 +39,7 @@ export default class Temp extends Component { return ( <View style={styles.container}> <Text style={styles.welcome}>{formatDateForViewHeader(cycleDay.date)}</Text> - <Text>Cycle day {getCycleDay()}</Text> + <Text>Cycle day {getCycleDayNumber()}</Text> <Text>Temperature</Text> <TextInput placeholder="Enter temperature" diff --git a/test/app.spec.js b/test/app.spec.js deleted file mode 100644 index 019e3fa032f7b89dcec2c4d8e8e7fc3b4d4111c5..0000000000000000000000000000000000000000 --- a/test/app.spec.js +++ /dev/null @@ -1,5 +0,0 @@ -import { expect } from 'chai' - -it('says hello world', function () { - expect(1 + 5).to.eql(6) -}) diff --git a/test/get-cycle-day.spec.js b/test/get-cycle-day.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..222d62e95fdf92ba56085fac0738310176cba2a9 --- /dev/null +++ b/test/get-cycle-day.spec.js @@ -0,0 +1,146 @@ +import chai from 'chai' +import dirtyChai from 'dirty-chai' + +const expect = chai.expect +chai.use(dirtyChai) + +import getCycleDayNumberModule from '../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