diff --git a/app.js b/app.js index bb31b220c91779d72a1c6e9198975e2e97147dfb..fae73d637f7ab50ddc879d8a8938d616d4cbc7b1 100644 --- a/app.js +++ b/app.js @@ -5,6 +5,7 @@ import Calendar from './components/calendar' import CycleDay from './components/cycle-day' import Chart from './components/chart/chart' import Settings from './components/settings' +import Stats from './components/stats' // this is until react native fixes this bugg, see // https://github.com/facebook/react-native/issues/18868#issuecomment-382671739 @@ -16,5 +17,6 @@ export default createStackNavigator({ calendar: { screen: Calendar }, cycleDay: { screen: CycleDay }, chart: { screen: Chart }, - settings: { screen: Settings } + settings: { screen: Settings }, + stats: { screen: Stats} }) diff --git a/components/calendar.js b/components/calendar.js index 67e52997a831a01fa3d850b0b5c9bd84687e440a..b447bf24cebd82d0d87a8b3c155c0df4885a5c78 100644 --- a/components/calendar.js +++ b/components/calendar.js @@ -1,6 +1,6 @@ import React, { Component } from 'react' import { View } from 'react-native' -import { Calendar } from 'react-native-calendars' +import { CalendarList } from 'react-native-calendars' import * as styles from '../styles' import { getOrCreateCycleDay, bleedingDaysSortedByDate } from '../db' @@ -35,7 +35,7 @@ export default class CalendarView extends Component { render() { return ( <View style={styles.container}> - <Calendar + <CalendarList onDayPress={ this.passDateToDayView.bind(this) } markedDates = { this.state.bleedingDaysInCalFormat } markingType = {'period'} diff --git a/components/home.js b/components/home.js index 623821d4e15b92297e9869831fb624c89ebbc182..315ea5046ac307bc0e768f2e9aa3e9ba57b14ba1 100644 --- a/components/home.js +++ b/components/home.js @@ -87,6 +87,12 @@ export default class Home extends Component { title="delete everything"> </Button> </View> + <View style={styles.homeButton}> + <Button + onPress={() => navigate('stats')} + title="Go to stats"> + </Button> + </View> </View> </ScrollView> ) diff --git a/components/stats.js b/components/stats.js new file mode 100644 index 0000000000000000000000000000000000000000..4021fdee0af44ab2fb229e565988014ba4a4d99f --- /dev/null +++ b/components/stats.js @@ -0,0 +1,51 @@ +import React, { Component } from 'react' +import { + Text, + View, + ScrollView +} from 'react-native' +import { LocalDate, ChronoUnit } from 'js-joda' +import styles from '../styles/index' +import cycleModule from '../lib/cycle' +import getCycleInfo from '../lib/cycle-length' + +export default class Stats extends Component { + render() { + const allMensesStarts = cycleModule().getAllMensesStarts() + const statsText = determineStatsText(allMensesStarts) + return ( + <ScrollView> + <View> + <Text style={styles.stats}>{statsText}</Text> + </View> + </ScrollView> + ) + } +} + +function getCycleLength(cycleStartDates) { + const cycleLengths = [] + for (let i = 0; i < cycleStartDates.length - 1; i++) { + const nextCycleStart = LocalDate.parse(cycleStartDates[i]) + const cycleStart = LocalDate.parse(cycleStartDates[i + 1]) + cycleLengths.push(cycleStart.until(nextCycleStart, ChronoUnit.DAYS)) + } + return cycleLengths +} + +function determineStatsText(allMensesStarts) { + const emptyStats = 'At least one completed cycle is needed to present you with stats here.' + if (allMensesStarts.length < 2) { + return emptyStats + } else { + const cycleLengths = getCycleLength(allMensesStarts) + const numberOfCycles = cycleLengths.length + if (numberOfCycles === 1) { + return `You have documented one cycle of ${cycleLengths[0]} days.` + } + const cycleInfo = getCycleInfo(cycleLengths) + const statsText = `Stats are based on ${numberOfCycles} completed cycles.\n\n\ + Average cycle length: ${cycleInfo.mean} days\n\nShortest cycle: ${cycleInfo.minimum} days\nLongest cycle: ${cycleInfo.maximum} days\nMedian length (meaning 50% of cycles are of this length or shorter): ${cycleInfo.median} days\nStandard deviation: ${cycleInfo.stdDeviation}` + return statsText + } +} \ No newline at end of file diff --git a/lib/period-length.js b/lib/cycle-length.js similarity index 67% rename from lib/period-length.js rename to lib/cycle-length.js index e62f076ef09aa0a555c62219fbd3a33c5e90baba..6f8feb56f54f4ecd70bafe7fe9648133314178cb 100644 --- a/lib/period-length.js +++ b/lib/cycle-length.js @@ -1,38 +1,38 @@ import assert from 'assert' -export default function getPeriodLengthStats(cycleLengths) { +export default function getCycleLengthStats(cycleLengths) { throwIfArgsAreNotInRequiredFormat(cycleLengths) - const periodLengthStats = {} + const cycleLengthStats = {} const sortedCycleLengths = cycleLengths.sort((a, b) => { return a - b }) - periodLengthStats.minimum = sortedCycleLengths[0] - periodLengthStats.maximum = sortedCycleLengths[cycleLengths.length - 1] - periodLengthStats.mean = Math.round( + cycleLengthStats.minimum = sortedCycleLengths[0] + cycleLengthStats.maximum = sortedCycleLengths[cycleLengths.length - 1] + cycleLengthStats.mean = Math.round( cycleLengths.reduce(getSum) / cycleLengths.length * 100 ) / 100 // median if (cycleLengths.length % 2 == 1) { - periodLengthStats.median = sortedCycleLengths[ + cycleLengthStats.median = sortedCycleLengths[ (cycleLengths.length + 1) / 2 - 1 ] } else { const middle = cycleLengths.length / 2 - periodLengthStats.median = (sortedCycleLengths[middle - 1] + + cycleLengthStats.median = (sortedCycleLengths[middle - 1] + sortedCycleLengths[middle]) / 2 } // corrected standard deviation (based on unbiased sample variance) if (cycleLengths.length > 1) { const sumOfSquares = cycleLengths.map(cycleLength => { - return Math.pow(cycleLength - periodLengthStats.mean, 2) + return Math.pow(cycleLength - cycleLengthStats.mean, 2) }).reduce(getSum) - periodLengthStats.stdDeviation = Math.round( + cycleLengthStats.stdDeviation = Math.round( Math.sqrt(sumOfSquares / (cycleLengths.length - 1 )) * 100 ) / 100 } else { - periodLengthStats.stdDeviation = null + cycleLengthStats.stdDeviation = null } - return periodLengthStats + return cycleLengthStats } function getSum(total, num) { diff --git a/lib/cycle.js b/lib/cycle.js index f350ff827e0b17e3aeb17c185d48e308e1980f21..23bc3cda757e3f8934cc06c85e12c3445906bdaa 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -130,10 +130,24 @@ export default function config(opts) { } } + function getAllMensesStarts(day, collectedDates) { + day = day || LocalDate.now().toString() + collectedDates = collectedDates || [] + const lastStart = getLastMensesStart(day) + if (!lastStart) { + return collectedDates + } else { + const newDay = LocalDate.parse(lastStart.date).minusDays(1).toString() + collectedDates.push(lastStart.date) + return getAllMensesStarts(newDay, collectedDates) + } + } + return { getCycleDayNumber, getCycleForDay, getPreviousCycle, - getCyclesBefore + getCyclesBefore, + getAllMensesStarts } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b0cdbd301e71bc399aacc11e595c7e9856d894d0..6c10f49e6e529628e01e79e337d991857e43820e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3479,8 +3479,8 @@ "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", "optional": true, "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" + "nan": "2.10.0", + "node-pre-gyp": "0.10.0" }, "dependencies": { "abbrev": { @@ -3502,19 +3502,21 @@ "bundled": true, "optional": true, "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "delegates": "1.0.0", + "readable-stream": "2.3.6" } }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { - "balanced-match": "^1.0.0", + "balanced-match": "1.0.0", "concat-map": "0.0.1" } }, @@ -3525,15 +3527,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3568,7 +3573,7 @@ "bundled": true, "optional": true, "requires": { - "minipass": "^2.2.1" + "minipass": "2.2.4" } }, "fs.realpath": { @@ -3581,14 +3586,14 @@ "bundled": true, "optional": true, "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" } }, "glob": { @@ -3596,12 +3601,12 @@ "bundled": true, "optional": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" } }, "has-unicode": { @@ -3614,7 +3619,7 @@ "bundled": true, "optional": true, "requires": { - "safer-buffer": "^2.1.0" + "safer-buffer": "2.1.2" } }, "ignore-walk": { @@ -3622,7 +3627,7 @@ "bundled": true, "optional": true, "requires": { - "minimatch": "^3.0.4" + "minimatch": "3.0.4" } }, "inflight": { @@ -3630,13 +3635,14 @@ "bundled": true, "optional": true, "requires": { - "once": "^1.3.0", - "wrappy": "1" + "once": "1.4.0", + "wrappy": "1.0.2" } }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3646,8 +3652,9 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { - "number-is-nan": "^1.0.0" + "number-is-nan": "1.0.1" } }, "isarray": { @@ -3658,20 +3665,23 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "1.1.11" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" + "safe-buffer": "5.1.1", + "yallist": "3.0.2" } }, "minizlib": { @@ -3679,12 +3689,13 @@ "bundled": true, "optional": true, "requires": { - "minipass": "^2.2.1" + "minipass": "2.2.4" } }, "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3699,9 +3710,9 @@ "bundled": true, "optional": true, "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" + "debug": "2.6.9", + "iconv-lite": "0.4.21", + "sax": "1.2.4" } }, "node-pre-gyp": { @@ -3709,16 +3720,16 @@ "bundled": true, "optional": true, "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.0", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.7", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.1" } }, "nopt": { @@ -3726,8 +3737,8 @@ "bundled": true, "optional": true, "requires": { - "abbrev": "1", - "osenv": "^0.1.4" + "abbrev": "1.1.1", + "osenv": "0.1.5" } }, "npm-bundled": { @@ -3740,8 +3751,8 @@ "bundled": true, "optional": true, "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" } }, "npmlog": { @@ -3749,15 +3760,16 @@ "bundled": true, "optional": true, "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" } }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3767,8 +3779,9 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { - "wrappy": "1" + "wrappy": "1.0.2" } }, "os-homedir": { @@ -3786,8 +3799,8 @@ "bundled": true, "optional": true, "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" } }, "path-is-absolute": { @@ -3805,10 +3818,10 @@ "bundled": true, "optional": true, "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" + "deep-extend": "0.5.1", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" }, "dependencies": { "minimist": { @@ -3823,13 +3836,13 @@ "bundled": true, "optional": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" } }, "rimraf": { @@ -3837,7 +3850,7 @@ "bundled": true, "optional": true, "requires": { - "glob": "^7.0.5" + "glob": "7.1.2" } }, "safe-buffer": { @@ -3872,10 +3885,11 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } }, "string_decoder": { @@ -3883,14 +3897,14 @@ "bundled": true, "optional": true, "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "5.1.1" } }, "strip-ansi": { "version": "3.0.1", "bundled": true, "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "2.1.1" } }, "strip-json-comments": { @@ -3903,13 +3917,13 @@ "bundled": true, "optional": true, "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", - "yallist": "^3.0.2" + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.1", + "yallist": "3.0.2" } }, "util-deprecate": { @@ -3922,7 +3936,7 @@ "bundled": true, "optional": true, "requires": { - "string-width": "^1.0.2" + "string-width": "1.0.2" } }, "wrappy": { diff --git a/styles/index.js b/styles/index.js index 1b09eb4c37ec79d41f94ab2a9a4e117ec1f9eb6a..fde1700a6900a5120d887c67c0830a20068bbf71 100644 --- a/styles/index.js +++ b/styles/index.js @@ -82,5 +82,11 @@ export default StyleSheet.create({ marginTop: 15, marginLeft: 'auto', marginRight: 'auto' + }, + stats: { + fontSize: 18, + margin: 30, + textAlign: 'left', + textAlignVertical: 'center' } }) \ No newline at end of file diff --git a/test/period-length.spec.js b/test/cycle-length.spec.js similarity index 65% rename from test/period-length.spec.js rename to test/cycle-length.spec.js index f10fb1854441b9d3578b7c6ed7384ab357269817..043cf96d34a355993549b1c400e99bb509645d63 100644 --- a/test/period-length.spec.js +++ b/test/cycle-length.spec.js @@ -1,14 +1,14 @@ import chai from 'chai' import { AssertionError } from 'assert' -import periodInfo from '../lib/period-length' +import cycleInfo from '../lib/cycle-length' const expect = chai.expect -describe('getPeriodLengthStats', () => { +describe('getCycleLengthStats', () => { it('works for a simple odd-numbered array', () => { - const periodLengths = [99, 5, 1, 2, 100] - const result = periodInfo(periodLengths) + const cycleLengths = [99, 5, 1, 2, 100] + const result = cycleInfo(cycleLengths) const expectedResult = { minimum: 1, maximum: 100, @@ -20,8 +20,8 @@ describe('getPeriodLengthStats', () => { }) it('works for a simple even-numbered array', () => { - const periodLengths = [4, 1, 15, 2, 20, 5] - const result = periodInfo(periodLengths) + const cycleLengths = [4, 1, 15, 2, 20, 5] + const result = cycleInfo(cycleLengths) const expectedResult = { minimum: 1, maximum: 20, @@ -32,8 +32,8 @@ describe('getPeriodLengthStats', () => { expect(result).to.eql(expectedResult) }) it('works for an one-element array', () => { - const periodLengths = [42] - const result = periodInfo(periodLengths) + const cycleLengths = [42] + const result = cycleInfo(cycleLengths) const expectedResult = { minimum: 42, maximum: 42, @@ -45,20 +45,20 @@ describe('getPeriodLengthStats', () => { }) describe('when args are wrong', () => { it('throws when arg object is an empty array', () => { - const periodLengths = [] - expect(() => periodInfo(periodLengths).to.throw(AssertionError)) + const cycleLengths = [] + expect(() => cycleInfo(cycleLengths).to.throw(AssertionError)) }) it('throws when arg object is not in right format', () => { const wrongObject = { hello: 'world' } - expect(() => periodInfo(wrongObject).to.throw(AssertionError)) + expect(() => cycleInfo(wrongObject).to.throw(AssertionError)) }) it('throws when arg array contains a string', () => { const wrongElement = [4, 1, 15, '2', 20, 5] - expect(() => periodInfo(wrongElement).to.throw(AssertionError)) + expect(() => cycleInfo(wrongElement).to.throw(AssertionError)) }) it('throws when arg array contains a NaN', () => { const wrongElement = [4, 1, 15, NaN, 20, 5] - expect(() => periodInfo(wrongElement).to.throw(AssertionError)) + expect(() => cycleInfo(wrongElement).to.throw(AssertionError)) }) }) }) \ No newline at end of file