diff --git a/package.json b/package.json index 0bca5f1bf..5b9cf761d 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,9 @@ "smoke-test:ci": "start-server-and-test server http://localhost:4000 smoke-test", "diff-lang": "./scripts/diff-lang.js" }, + "jest": { + "transformIgnorePatterns": [] + }, "dependencies": { "@babel/core": "^7.3.4", "@babel/plugin-proposal-class-properties": "^7.3.4", diff --git a/src/components/frequencies/functions.js b/src/components/frequencies/functions.js index c4f9ca716..41efa4377 100644 --- a/src/components/frequencies/functions.js +++ b/src/components/frequencies/functions.js @@ -2,15 +2,15 @@ import { select, mouse } from "d3-selection"; import 'd3-transition'; import scaleLinear from "d3-scale/src/linear"; import { axisBottom, axisLeft } from "d3-axis"; +import { min, max } from "d3-array"; import { rgb } from "d3-color"; import { area } from "d3-shape"; import { format } from "d3-format"; -import _range from "lodash/range"; import { dataFont } from "../../globalStyles"; import { unassigned_label } from "../../util/processFrequencies"; import { isColorByGenotype, decodeColorByGenotype } from "../../util/getGenotype"; import { numericToCalendar } from "../../util/dateHelpers"; -import { createDisplayDate, calculateMajorGridSeperationForTime } from "../tree/phyloTree/grid"; +import { computeTemporalGridPoints } from "../tree/phyloTree/grid"; /* C O N S T A N T S */ const opacity = 0.85; @@ -95,11 +95,9 @@ const removeProjectionInfo = (svg) => { export const drawXAxis = (svg, chartGeom, scales) => { const domain = scales.x.domain(), range = scales.x.range(); - const {majorStep} = calculateMajorGridSeperationForTime( - domain[1] - domain[0], - range[1] - range[0] + const {majorGridPoints} = computeTemporalGridPoints( + min(domain), max(domain), range[1] - range[0] ); - const customDate = (date) => createDisplayDate(majorStep, date); removeXAxis(svg); svg.append("g") .attr("class", "x axis") @@ -107,8 +105,9 @@ export const drawXAxis = (svg, chartGeom, scales) => { .style("font-family", dataFont) .style("font-size", "12px") .call(axisBottom(scales.x) - .tickValues(_range(domain[0], domain[1], majorStep)) - .tickFormat(customDate)); + .tickValues(majorGridPoints.map((x) => x.position)) + .tickFormat((_, i) => majorGridPoints[i].name) + ); }; export const drawYAxis = (svg, chartGeom, scales) => { diff --git a/src/components/tree/phyloTree/grid.js b/src/components/tree/phyloTree/grid.js index 3caf435de..22f1c0e41 100644 --- a/src/components/tree/phyloTree/grid.js +++ b/src/components/tree/phyloTree/grid.js @@ -3,8 +3,8 @@ import { min, max } from "d3-array"; import { transition } from "d3-transition"; import { easeLinear } from "d3-ease"; import { timerStart, timerEnd } from "../../../util/perf"; -import { months, animationInterpolationDuration } from "../../../util/globals"; -import { numericToCalendar } from "../../../util/dateHelpers"; +import { animationInterpolationDuration } from "../../../util/globals"; +import { numericToDateObject, calendarToNumeric, getPreviousDate, getNextDate, dateToString, prettifyDate } from "../../../util/dateHelpers"; export const hideGrid = function hideGrid() { if ("majorGrid" in this.groups) { @@ -46,111 +46,38 @@ const addSVGGroupsIfNeeded = (groups, svg) => { }; /** - * Create the major-grid-line separation for divergence scales. - * @param {numeric} range num years or amount of divergence present in current view + * Create the separation between major & minor grid lines for divergence scales. + * @param {numeric} range amount of divergence (subs/site/year _or_ num mutations) present in current view * @param {numeric} minorTicks num of minor ticks desired between each major step * @returns {object} * - property `majorStep` {numeric}: space between major x-axis gridlines (measure of divergence) * - property `minorStep` {numeric}: space between minor x-axis gridlines (measure of divergence) */ -const calculateMajorGridSeperationForDivergence = (range, minorTicks) => { +const calculateDivGridSeperation = (range, minorTicks) => { /* make an informed guess of the step size to start with. E.g. 0.07 => step of 0.01, 70 => step size of 10 */ const logRange = Math.floor(Math.log10(range)); let majorStep = Math.pow(10, logRange); // eslint-disable-line no-restricted-properties - if (range/majorStep < 2) { // if step > 0.5*range then make more fine-grained steps majorStep /= 5; } else if (range/majorStep <5) { // if step > 0.2*range then make more fine grained steps majorStep /= 2; } - let numMinorTicks = minorTicks; if (majorStep===5 || majorStep===10) { numMinorTicks = 5; } const minorStep = majorStep / numMinorTicks; - - return {majorStep, minorStep}; -}; - -/** - * Create the major-grid-line separation for temporal view. - * @param {numeric} timeRange num years in current view - * @param {numeric} pxAvailable number of pixels available for the x axis - * @returns {object} - * - property `majorStep` {numeric}: space between major x-axis gridlines (measure of time) - * - property `minorStep` {numeric}: space between minor x-axis gridlines (measure of time) - */ -export const calculateMajorGridSeperationForTime = (timeRange, pxAvailable) => { - const rountToNearest = (n, p) => Math.ceil(n/p)*p; - - const getMinorSpacing = (majorStep) => { - const timesToTry = [1/365.25, 1/52, 1/12, 1, 10, 100, 1000]; - for (const t of timesToTry) { - const n = majorStep / t; - // max number we allow is 12 (so that a major grid of a year can have minor grids of a month) - if (n <= 12) return t; - } - return majorStep; // fallthrough. Only happens for _very_ large trees - }; - - /* in general, we find that 1 major point for every ~100px works well - for wider displays we shift up to 150px then 200px */ - const nSteps = Math.floor(pxAvailable / (pxAvailable < 1200 ? 100 : 150)) || 1; - - let majorStep = timeRange / nSteps; - - /* For time views, it's nicer if the spacing is meaningful. - There's probably a better way to do this than cascading through levels */ - if (majorStep > 100) { - majorStep = rountToNearest(majorStep, 100); - } else if (majorStep > 10) { - majorStep = rountToNearest(majorStep, 10); - } else if (majorStep > 1) { - majorStep = rountToNearest(majorStep, 1); - } else if (majorStep > (1/12)) { - /* each step is longer than a month, but shorter than a year */ - majorStep = rountToNearest(majorStep, 1/12); - } else if (majorStep > (1/52)) { - /* each step is longer than a week, but shorter than a month */ - majorStep = rountToNearest(majorStep, 1/52); - } else if (majorStep > (1/365.25)) { - /* each time step is longer than a day, but shorter than a week */ - majorStep = rountToNearest(majorStep, 1/365.25); - } else { - majorStep = 1/365.25; - } - const minorStep = getMinorSpacing(majorStep); return {majorStep, minorStep}; }; -/** - * Format the date to be displayed below major gridlines - * @param {numeric} step num years between each major gridline. Can be decimal. - * @param {numeric} numDate date in decimal format - * @returns {string} date to be displayed below major gridline - */ -export const createDisplayDate = (step, numDate) => { - if (step >= 1) { - return numDate.toFixed(Math.max(0, -Math.floor(Math.log10(step)))); - } - const [year, month, day] = numericToCalendar(numDate).split("-"); - if (step >= 1/12) { - return `${year}-${months[month]}`; - } - return `${year}-${months[month]}-${day}`; -}; - - -const computeXGridPoints = (xmin, xmax, layout, distanceMeasure, minorTicks, pxAvailable) => { +const computeDivergenceGridPoints = (xmin, xmax, layout, minorTicks) => { const majorGridPoints = []; const minorGridPoints = []; /* step is the amount (same units of xmax, xmin) of seperation between major grid lines */ - const {majorStep, minorStep} = distanceMeasure === "num_date" ? - calculateMajorGridSeperationForTime(xmax-xmin, Math.abs(pxAvailable)) : - calculateMajorGridSeperationForDivergence(xmax-xmin, minorTicks); + const {majorStep, minorStep} = calculateDivGridSeperation(xmax-xmin, minorTicks); + const gridMin = Math.floor(xmin/majorStep)*majorStep; const minVis = layout==="radial" ? xmin : gridMin; const maxVis = xmax; @@ -159,9 +86,7 @@ const computeXGridPoints = (xmin, xmax, layout, distanceMeasure, minorTicks, pxA const pos = gridMin + majorStep*ii; majorGridPoints.push({ position: pos, - name: distanceMeasure === "num_date" ? - createDisplayDate(majorStep, pos) : - pos.toFixed(Math.max(0, -Math.floor(Math.log10(majorStep)))), + name: pos.toFixed(Math.max(0, -Math.floor(Math.log10(majorStep)))), visibility: ((posmaxVis)) ? "hidden" : "visible", axis: "x" }); @@ -177,10 +102,118 @@ const computeXGridPoints = (xmin, xmax, layout, distanceMeasure, minorTicks, pxA return {majorGridPoints, minorGridPoints}; }; +/** + * Calculate the spacing between Major and Minor grid points. This is computed via a + * heuristic which takes into account (a) the available space (pixels) and (b) the + * time range to display. + * As major grid lines are (usually) labelled, we wish these to represent a consistent + * spacing of time, e.g. "3 months" or "7 years". Note that this means the actual time between + * grids may be very slightly different, as months, years etc can have different numbers of days. + * @param {numeric} timeRange numeric date range in current view (between right-most tip & left-most node) + * @param {numeric} pxAvailable number of pixels available + * @returns {object} + */ +const calculateTemporalGridSeperation = (timeRange, pxAvailable) => { + const [majorStep, minorStep] = [{unit: "DAY", n: 1}, {unit: "DAY", n: 0}]; + const minPxBetweenMajorGrid = (pxAvailable < 1200 ? 200 : 300); + const timeBetweenMajorGrids = timeRange/(Math.floor(pxAvailable / minPxBetweenMajorGrid)); + const levels = { + CENTURY: {t: 100, max: undefined}, + DECADE: {t: 10, max: 5}, // i.e. spacing of 50 years is ok, but 60 jumps up to 100y spacing + FIVEYEAR: {t: 5, max: 1}, + YEAR: {t: 1, max: 3}, // 4 year spacing not allowed (will use 5 year instead) + MONTH: {t: 1/12, max: 6}, // 7 month spacing not allowed + WEEK: {t: 1/52, max: 1}, // 2 week spacing not allowed - prefer months + DAY: {t: 1/365, max: 3} + }; + const levelsKeys = Object.keys(levels); + + /* calculate the best unit of time to fit into the allowed range */ + majorStep.unit = "DAY"; // fallback value + for (let i=0; i levels[levelsKeys[i]].t) { + majorStep.unit = levelsKeys[i]; + break; + } + } + /* how many of those "units" should ideally fit into each major grid separation? */ + majorStep.n = Math.floor(timeBetweenMajorGrids/levels[majorStep.unit].t) || 1; + /* if the numer of units (per major grid) is above the allowed max, use a bigger unit */ + if (levels[majorStep.unit].max && majorStep.n > levels[majorStep.unit].max) { + majorStep.unit = levelsKeys[levelsKeys.indexOf(majorStep.unit)-1]; + majorStep.n = Math.floor(timeBetweenMajorGrids/levels[majorStep.unit].t) || 1; + } + + /* Calculate best unit of time for the minor grid spacing */ + if (majorStep.n > 1 || majorStep.unit === "DAY") { + minorStep.unit = majorStep.unit; + } else { + minorStep.unit = levelsKeys[levelsKeys.indexOf(majorStep.unit)+1]; + } + /* how many of those "units" should form the separation of the minor grids? */ + const majorSpacing = majorStep.n * levels[majorStep.unit].t; + minorStep.n = Math.ceil(levels[minorStep.unit].t/majorSpacing); + + return {majorStep, minorStep}; +}; + + +/** + * Compute the major & minor temporal grid points for display. + * @param {numeric} xmin numeric date of minimum value in view + * @param {numeric} xmax numeric date of maximum value in view + * @param {numeric} pxAvailable pixels in which to display the date range (xmin, xmax) + * @returns {Object} properties: `majorGridPoints`, `minorGridPoints` + */ +export const computeTemporalGridPoints = (xmin, xmax, pxAvailable) => { + const [majorGridPoints, minorGridPoints] = [[], []]; + const {majorStep, minorStep} = calculateTemporalGridSeperation(xmax-xmin, Math.abs(pxAvailable)); + + /* Major Grid Points */ + const overallStopDate = getNextDate(majorStep.unit, numericToDateObject(xmax)); + let proposedDate = getPreviousDate(majorStep.unit, numericToDateObject(xmin)); + while (proposedDate < overallStopDate) { + majorGridPoints.push({ + date: proposedDate, + position: calendarToNumeric(dateToString(proposedDate)), + name: prettifyDate(majorStep.unit, proposedDate), + visibility: 'visible', + axis: "x" + }); + for (let i=0; i { + proposedDate = getNextDate(minorStep.unit, majorGridPoint.date); + for (let i=0; i { const majorGridPoints = []; let yStep = 0; - yStep = calculateMajorGridSeperationForDivergence(ymax-ymin).majorStep; + yStep = calculateDivGridSeperation(ymax-ymin).majorStep; const precisionY = Math.max(0, -Math.floor(Math.log10(yStep))); const gridYMin = Math.floor(ymin/yStep)*yStep; const maxYVis = ymax; @@ -222,9 +255,9 @@ export const addGrid = function addGrid() { /* determine grid points (i.e. on the x/polar axis where lines/circles will be drawn through) Major grid points are thicker and have text Minor grid points have no text */ - const {majorGridPoints, minorGridPoints} = computeXGridPoints( - xmin, xmax, layout, this.distance, this.params.minorTicks, xAxisPixels - ); + const {majorGridPoints, minorGridPoints} = this.distance === "num_date" ? + computeTemporalGridPoints(xmin, xmax, xAxisPixels) : + computeDivergenceGridPoints(xmin, xmax, layout, this.params.minorTicks); /* HOF, which returns the fn which constructs the SVG path string to draw the axis lines (circles for radial trees). @@ -326,7 +359,6 @@ export const addGrid = function addGrid() { .style("stroke", this.params.minorGridStroke) .style("stroke-width", this.params.minorGridWidth); - /* draw the text labels for majorGridPoints */ this.groups.gridText.selectAll("*").remove(); this.svg.selectAll(".gridText").remove(); diff --git a/src/util/dateHelpers.js b/src/util/dateHelpers.js index 381b2c7cf..1d7fe5998 100644 --- a/src/util/dateHelpers.js +++ b/src/util/dateHelpers.js @@ -1,9 +1,31 @@ +import { months } from "./globals"; + +export const dateToString = (date) => { + return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; +}; + /** - * Convert a numeric date to a calendar date (which is nicer to display) - * This is (for CE dates) meant to be used as the inverse ofthe TreeTime + * Convert a numeric date to a `Date` object + * This is (for CE dates) meant to be used as the inverse of the TreeTime * function `numeric_date` which places the numeric date at noon (12h00), * i.e. Jan 1 is 0.5/365 of a year (if the year is not a leap year). * @param {numeric} numDate Numeric date + * @returns {Date} date object + */ +export const numericToDateObject = (numDate) => { + /* Beware: for `Date`, months are 0-indexed, days are 1-indexed */ + const fracPart = numDate%1; + const year = parseInt(numDate, 10); + const nDaysInYear = isLeapYear(year) ? 366 : 365; + const nDays = fracPart * nDaysInYear; + const date = new Date((new Date(year, 0, 1)).getTime() + nDays*24*60*60*1000); + return date; +}; + +/** + * Converts a numeric date to a calendar date (which is nicer to display). + * The inverse of `calendarToNumeric`. See also `numericToDateObject`. + * @param {numeric} numDate Numeric date * @returns {string} date in YYYY-MM-DD format for CE dates, YYYY for BCE dates */ export const numericToCalendar = (numDate) => { @@ -12,12 +34,7 @@ export const numericToCalendar = (numDate) => { return Math.round(numDate).toString(); } /* for CE dates, return string in YYYY-MM-DD format */ - /* Beware: for `Date`, months are 0-indexed, days are 1-indexed */ - const fracPart = numDate%1; - const year = parseInt(numDate, 10); - const nDaysInYear = isLeapYear(year) ? 366 : 365; - const nDays = fracPart * nDaysInYear; - const date = new Date((new Date(year, 0, 1)).getTime() + nDays*24*60*60*1000); + const date = numericToDateObject(numDate); return dateToString(date); }; @@ -47,11 +64,107 @@ export const currentCalDate = () => dateToString(new Date()); export const currentNumDate = () => calendarToNumeric(currentCalDate()); -function dateToString(date) { - return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; -} function isLeapYear(year) { return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0); } + +/** + * Get the previous date closest to the provided one by the specified `unit` (e.g. day, week, month...) + * Weeks are defined to start on a Monday (ISO week) + * The returned date should represent c. midday on that day + * NOTE: this function is not simply the inverse of `getNextDate`. We are returning the most recent date + * (for the given `unit` of time) from the provided `date`. The returned date may be equal to the provided `date`! + * For instance, the previous WEEK from Monday the 10th is Monday the 10th!, the previous WEEK of Tuesday 11th is Monday 10th. + * @param {str} unit time unit to advance to (day, week, month, year, century) + * @param {Date} date JavaScript Date Object + * @returns {Date} a new Javascript date object. Note that @param `date` isn't modified. + */ +export const getPreviousDate = (unit, date) => { + const dateClone = new Date(date.getTime()); + const jan1st = date.getDate()===1 && date.getMonth()===0; + switch (unit) { + case "DAY": + return dateClone; + case "WEEK": + const dayIdx = date.getDay(); // 0 is sunday + if (dayIdx===1) return dateClone; + dateClone.setDate(date.getDate() + (8-dayIdx)%7 - 7); + return dateClone; + case "MONTH": + if (date.getDate()===1) return dateClone; // i.e. 1st of the month + return new Date(date.getFullYear(), date.getMonth(), 1, 12); + case "YEAR": + if (jan1st) return dateClone; + return new Date(date.getFullYear(), 0, 1, 12); + case "FIVEYEAR": // fallsthrough + case "DECADE": + // decades start at "nice" numbers - i.e. multiples of 5 -- e.g. 2014 -> 2010, 2021 -> 2020 + return new Date(Math.floor((date.getFullYear())/5)*5, 0, 1, 12); + case "CENTURY": + return new Date(Math.floor((date.getFullYear())/100)*100, 0, 1, 12); + default: + console.error("Unknown unit for `advanceDateTo`:", unit); + return dateClone; + } +}; + +/** + * Returns a `Date` object one `unit` in the future of the provided `date` + */ +export const getNextDate = (unit, date) => { + const dateClone = new Date(date.getTime()); + switch (unit) { + case "DAY": + dateClone.setDate(date.getDate() + 1); + break; + case "WEEK": + dateClone.setDate(date.getDate() + 7); + break; + case "MONTH": + dateClone.setMonth(date.getMonth() + 1); + break; + case "YEAR": + dateClone.setFullYear(date.getFullYear() + 1); + break; + case "FIVEYEAR": + dateClone.setFullYear(date.getFullYear() + 5); + break; + case "DECADE": + dateClone.setFullYear(date.getFullYear() + 10); + break; + case "CENTURY": + dateClone.setFullYear(date.getFullYear() + 100); + break; + default: + console.error("Unknown unit for `getNextDate`:", unit); + } + return dateClone; +}; + +/** + * Format the date to be displayed below major gridlines. + * @param {string} unit CENTURY, DECADE, YEAR etc + * @param {numeric | string | Date} date can be numeric (2016.123), string (YYYY-MM-DD) or a Date object + * @returns {string} prettified date for display + */ +export const prettifyDate = (unit, date) => { + const stringDate = typeof date ==="number" ? numericToCalendar(date) : + date instanceof Date ? dateToString(date) : + date; + const [year, month, day] = stringDate.split("-"); + switch (unit) { + case "CENTURY": // falls through + case "DECADE": // falls through + case "FIVEYEAR": // falls through + case "YEAR": + if (month==="01" && day==="01") return year; + // falls through if not jan 1st + case "MONTH": + if (day==="01") return `${year}-${months[month]}`; + // falls through if not 1st of month + default: + return `${year}-${months[month]}-${day}`; + } +}; diff --git a/test/dates.test.js b/test/dates.test.js new file mode 100644 index 000000000..8ae5e9154 --- /dev/null +++ b/test/dates.test.js @@ -0,0 +1,94 @@ +import { numericToCalendar, calendarToNumeric, getPreviousDate, numericToDateObject, dateToString, getNextDate, prettifyDate} from "../src/util/dateHelpers"; + +/* numeric data computed from augur 10.0.4, treetime 0.7.4 */ +const augurDates = { + "1900-01-01": 1900.0013698630137, + "1900-12-31": 1900.9986301369863, + "1904-02-29": 1904.1625683060108, + "1904-12-31": 1904.9986338797814, + "2000-01-01": 2000.0013661202186, + "2000-06-01": 2000.4166666666667, + "2000-12-31": 2000.9986338797814, + "2016-11-01": 2016.8346994535518, + "2020-01-01": 2020.0013661202186, + "2020-02-29": 2020.1625683060108, + "2020-06-01": 2020.4166666666667, + "2020-12-31": 2020.9986338797814 +}; + +/** + * JSONs encode dates as floats (`num_date`) via augur. + * This tests the auspice function `numericToCalendar` + * which converts those to calendar (YYYY-MM-DD) dates. + */ +test("Numeric -> calendar date parsing is the same as in augur", () => { + Object.entries(augurDates).forEach(([cal, num]) => { + expect(numericToCalendar(num)).toStrictEqual(cal); + }); +}); + +/** + * Test that auspice converts calendar dates to the same + * (numeric) value as augur. Tiny rounding errors are allowed. + */ +test("Calendar -> numeric date parsing is the same as in augur", () => { + Object.entries(augurDates).forEach(([cal, num]) => { + expect(calendarToNumeric(cal)).toBeCloseTo(num); + }); +}); + +/** + * Auspice provides `calendarToNumeric` which is intended to be the inverse + * of `numericToCalendar`. + */ +test("calendarToNumeric is the inverse of numericToCalendar", () => { + Object.keys(augurDates).forEach((calendarDate) => { + expect(calendarDate).toStrictEqual(numericToCalendar(calendarToNumeric(calendarDate))); + }); +}); + +test("Numeric dates can be converted to calendar dates and back again", () => { + Object.values(augurDates).forEach((numericDate) => { + const convertedNumDate = calendarToNumeric(numericToCalendar(numericDate)); + expect(numericDate).toBeCloseTo(convertedNumDate); + }); +}); + +test("getPreviousDate to nearest ", () => { + const data = [ + {day: "2020-11-11", week: "2020-11-09", month: "2020-11-01", year: "2020-01-01", decade: "2020-01-01", century: "2000-01-01"}, + {day: "2020-12-03", week: "2020-11-30", month: "2020-12-01", year: "2020-01-01", decade: "2020-01-01", century: "2000-01-01"}, + {day: "2019-05-20", week: "2019-05-20", month: "2019-05-01", year: "2019-01-01", decade: "2015-01-01", century: "2000-01-01"} + ]; + data.forEach((d) => { + const dateObj = numericToDateObject(calendarToNumeric(d.day)); + Object.entries(d).forEach(([key, expectedDateString]) => { + const unit = key.toUpperCase(); // e.g. "DAY", "YEAR" + const shiftedDateString = dateToString(getPreviousDate(unit, dateObj)); + expect(shiftedDateString).toStrictEqual(expectedDateString); + }); + }); +}); + +test("getNextDate", () => { + const data = [ + ["2020-12-11", {day: "2020-12-12", week: "2020-12-18", month: "2021-01-11", year: "2021-12-11", decade: "2030-12-11", century: "2120-12-11"}] + ]; + data.forEach(([providedDate, nextDates]) => { + const dateObj = numericToDateObject(calendarToNumeric(providedDate)); + Object.entries(nextDates).forEach(([key, expectedDateString]) => { + const unit = key.toUpperCase(); // e.g. "DAY", "YEAR" + const shiftedDateString = dateToString(getNextDate(unit, dateObj)); + expect(shiftedDateString).toStrictEqual(expectedDateString); + }); + }); +}); + + +test("dates are prettified as expected", () => { + expect(prettifyDate("DAY", "2020-01-05")).toStrictEqual("2020-Jan-05"); + expect(prettifyDate("YEAR", "2020-01-05")).toStrictEqual("2020-Jan-05"); // Not "2020" as we don't provide Jan 1st + expect(prettifyDate("YEAR", "2020-01-01")).toStrictEqual("2020"); + expect(prettifyDate("MONTH", "2020-01-05")).toStrictEqual("2020-Jan-05"); + expect(prettifyDate("MONTH", "2020-01-01")).toStrictEqual("2020-Jan"); +});