From e91597a06425cc2f78be1741bab0530022dd7cdd Mon Sep 17 00:00:00 2001 From: Souler Date: Thu, 26 Sep 2024 09:22:08 +0200 Subject: [PATCH 1/5] refactor: remove moment as a dependency --- format.js | 111 ++++++++++++++++++++++++++++++++++++++++++++ index.js | 4 +- package.json | 3 -- test/format.spec.js | 95 +++++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 format.js create mode 100644 test/format.spec.js diff --git a/format.js b/format.js new file mode 100644 index 0000000..7bb0c26 --- /dev/null +++ b/format.js @@ -0,0 +1,111 @@ +const util = require('util'); + +/** + * Pads the provided number into a string of 2 digits; adding leading + * zeros if needed. + * Values resulting in more than 2 digits are left untouched. + * + * @param {string} number + * @returns A two-digit number + * @throws {TypeError} if the provided value is not a valid integer + */ +function formatTwoDigits(number) { + if (!Number.isInteger(number)) { + throw new TypeError(`Not a valid integer: ${number}`); + } + + const n = String(number); + + return n.padStart(2, '0'); +} + +/** + * Returns a [short](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#month) + * (3-letters) representation of the given month index **in English**. + * @param {number} month + * @returns {string} + * @throws {TypeError} if the provided value is not a valid month number + */ +function formatShortMonth(month) { + switch (month) { + // Note: January is month 0! + case 0: + return 'Jan'; + case 1: + return 'Feb'; + case 2: + return 'Mar'; + case 3: + return 'Apr'; + case 4: + return 'May'; + case 5: + return 'Jun'; + case 6: + return 'Jul'; + case 7: + return 'Aug'; + case 8: + return 'Sep'; + case 9: + return 'Oct'; + case 10: + return 'Nov'; + case 11: + return 'Dec'; + default: + throw new TypeError(`Not a valid month value: ${month}`); + } +} + +/** + * Returns a [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) compliant + * offset string. + * + * @returns {string} + * @throws {TypeError} if the provided value is not a valid integer + */ +function formatOffset(offsetMinutes) { + if (!Number.isInteger(offsetMinutes)) { + throw new TypeError(`Not a valid integer: ${offsetMinutes}`); + } + + const absoluteOffset = Math.abs(offsetMinutes); + const hours = formatTwoDigits(Math.floor(absoluteOffset / 60)); + const minutes = formatTwoDigits(absoluteOffset % 60); + const sign = offsetMinutes >= 0 ? '-' : '+'; + + return `${sign}${hours}${minutes}`; +} + +/** + * Formats the provided date into a [Common Log Format](https://en.wikipedia.org/wiki/Common_Log_Format) + * compliant string. + * + * @param {Date} date + * @returns {string} + */ +function formatCommonAccessLogDate(date) { + if (!(date instanceof Date)) { + throw new TypeError('Not a valid date'); + } + + // e.g: 10/Oct/2000:13:55:36 -0700 + return util.format( + '%s/%s/%s:%s:%s:%s %s', + formatTwoDigits(date.getDate()), + formatShortMonth(date.getMonth()), + date.getFullYear(), + formatTwoDigits(date.getHours()), + formatTwoDigits(date.getMinutes()), + formatTwoDigits(date.getSeconds()), + formatOffset(date.getTimezoneOffset()) + ); +} + +module.exports = { + formatCommonAccessLogDate, + formatOffset, + formatShortMonth, + formatTwoDigits +}; diff --git a/index.js b/index.js index 3d37615..b445a3c 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ const util = require('util'); -const moment = require('moment'); +const { formatCommonAccessLogDate } = require('./format'); module.exports = function (stream) { if (!stream) stream = process.stdout; @@ -13,7 +13,7 @@ module.exports = function (stream) { // eslint-disable-next-line unicorn/explicit-length-check const length = ctx.length ? ctx.length.toString() : '-'; - const date = moment().format('D/MMM/YYYY:HH:mm:ss ZZ'); + const date = formatCommonAccessLogDate(new Date()); stream.write( util.format( diff --git a/package.json b/package.json index 535268d..9bf1eb6 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,6 @@ "3imed-jaberi (https://www.3imed-jaberi.com)" ], "license": "MIT", - "dependencies": { - "moment": "^2.26.0" - }, "devDependencies": { "eslint-config-xo-lass": "^1.0.3", "koa": "^2.12.1", diff --git a/test/format.spec.js b/test/format.spec.js new file mode 100644 index 0000000..5b7d329 --- /dev/null +++ b/test/format.spec.js @@ -0,0 +1,95 @@ +const { + formatCommonAccessLogDate, + formatOffset, + formatShortMonth, + formatTwoDigits, +} = require('../format') + +describe('formatTwoDigits', () => { + test.each([ + { value: 0, expected: '00' }, + { value: 1, expected: '01' }, + { value: 11, expected: '11' }, + { value: 99, expected: '99' }, + { value: 123, expected: '123' }, + ])('returns $expected for $value', ({ value, expected }) => { + expect(formatTwoDigits(value)).toBe(expected) + }) + + test('throws TypeError if provided value is Infinity', () => { + expect(() => formatTwoDigits(Infinity)).toThrow(TypeError) + }) + + test('throws TypeError if provided value is NaN', () => { + expect(() => formatTwoDigits(NaN)).toThrow(TypeError) + }) + + test('throws TypeError if provided value is a decimal number', () => { + expect(() => formatTwoDigits(0.99)).toThrow(TypeError) + }) + + test('throws TypeError if provided value is a string', () => { + expect(() => formatTwoDigits('0')).toThrow(TypeError) + }) +}) + +describe('formatOffset', () => { + test.each([ + { value: 480, expected: '-0800' }, + { value: 0, expected: '+0000' }, + { value: -180, expected: '+0300' }, + ])('returns $expected for $value', ({ value, expected }) => { + expect(formatOffset(value)).toBe(expected) + }) + + test('throws TypeError if provided value is Infinity', () => { + expect(() => formatOffset(Infinity)).toThrow(TypeError) + }) + + test('throws TypeError if provided value is NaN', () => { + expect(() => formatOffset(NaN)).toThrow(TypeError) + }) + + test('throws TypeError if provided value is a decimal number', () => { + expect(() => formatOffset(60.5)).toThrow(TypeError) + }) + + test('throws TypeError if provided value is a string', () => { + expect(() => formatOffset('60')).toThrow(TypeError) + }) +}) + +describe('formatShortMonth', () => { + test.each([ + { value: 0, expected: 'Jan' }, + { value: 1, expected: 'Feb' }, + { value: 2, expected: 'Mar' }, + { value: 3, expected: 'Apr' }, + { value: 4, expected: 'May' }, + { value: 5, expected: 'Jun' }, + { value: 6, expected: 'Jul' }, + { value: 7, expected: 'Aug' }, + { value: 8, expected: 'Sep' }, + { value: 9, expected: 'Oct' }, + { value: 10, expected: 'Nov' }, + { value: 11, expected: 'Dec' }, + ])('returns $expected for $value', ({ value, expected }) => { + expect(formatShortMonth(value)).toBe(expected) + }) + + test('throws TypeError for a non-valid month number', () => { + expect(() => formatShortMonth(13)).toThrow(TypeError) + }) +}) + +describe('formatCommonAccessLogDate', () => { + test('correctly formats a date', () => { + const date = new Date('2020-01-01T12:34:56Z') + const expectedValue = '01/Jan/2020:10:34:56 +0200' + expect(formatCommonAccessLogDate(date)).toBe(expectedValue) + }) + + test('throws TypeError for non-Date value', () => { + expect(() => formatCommonAccessLogDate({})).toThrow(TypeError) + }) +}) \ No newline at end of file From 43f257699c3665c9c86ba3fd4e0b2a34e1cd45fc Mon Sep 17 00:00:00 2001 From: Souler Date: Sat, 12 Oct 2024 13:55:10 +0200 Subject: [PATCH 2/5] refactor: rename formatting functions As requested by @3imed-jaberi on PR #11 --- format.js | 32 ++++++++++++++++---------------- index.js | 4 ++-- test/format.spec.js | 44 ++++++++++++++++++++++---------------------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/format.js b/format.js index 7bb0c26..44922c6 100644 --- a/format.js +++ b/format.js @@ -9,7 +9,7 @@ const util = require('util'); * @returns A two-digit number * @throws {TypeError} if the provided value is not a valid integer */ -function formatTwoDigits(number) { +function toTwoDigits(number) { if (!Number.isInteger(number)) { throw new TypeError(`Not a valid integer: ${number}`); } @@ -26,7 +26,7 @@ function formatTwoDigits(number) { * @returns {string} * @throws {TypeError} if the provided value is not a valid month number */ -function formatShortMonth(month) { +function toShortMonth(month) { switch (month) { // Note: January is month 0! case 0: @@ -65,14 +65,14 @@ function formatShortMonth(month) { * @returns {string} * @throws {TypeError} if the provided value is not a valid integer */ -function formatOffset(offsetMinutes) { +function toOffset(offsetMinutes) { if (!Number.isInteger(offsetMinutes)) { throw new TypeError(`Not a valid integer: ${offsetMinutes}`); } const absoluteOffset = Math.abs(offsetMinutes); - const hours = formatTwoDigits(Math.floor(absoluteOffset / 60)); - const minutes = formatTwoDigits(absoluteOffset % 60); + const hours = toTwoDigits(Math.floor(absoluteOffset / 60)); + const minutes = toTwoDigits(absoluteOffset % 60); const sign = offsetMinutes >= 0 ? '-' : '+'; return `${sign}${hours}${minutes}`; @@ -85,7 +85,7 @@ function formatOffset(offsetMinutes) { * @param {Date} date * @returns {string} */ -function formatCommonAccessLogDate(date) { +function toCommonAccessLogDateFormat(date) { if (!(date instanceof Date)) { throw new TypeError('Not a valid date'); } @@ -93,19 +93,19 @@ function formatCommonAccessLogDate(date) { // e.g: 10/Oct/2000:13:55:36 -0700 return util.format( '%s/%s/%s:%s:%s:%s %s', - formatTwoDigits(date.getDate()), - formatShortMonth(date.getMonth()), + toTwoDigits(date.getDate()), + toShortMonth(date.getMonth()), date.getFullYear(), - formatTwoDigits(date.getHours()), - formatTwoDigits(date.getMinutes()), - formatTwoDigits(date.getSeconds()), - formatOffset(date.getTimezoneOffset()) + toTwoDigits(date.getHours()), + toTwoDigits(date.getMinutes()), + toTwoDigits(date.getSeconds()), + toOffset(date.getTimezoneOffset()) ); } module.exports = { - formatCommonAccessLogDate, - formatOffset, - formatShortMonth, - formatTwoDigits + toCommonAccessLogDateFormat, + toOffset, + toShortMonth, + toTwoDigits }; diff --git a/index.js b/index.js index b445a3c..08d681c 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ const util = require('util'); -const { formatCommonAccessLogDate } = require('./format'); +const { toCommonAccessLogDateFormat } = require('./format'); module.exports = function (stream) { if (!stream) stream = process.stdout; @@ -13,7 +13,7 @@ module.exports = function (stream) { // eslint-disable-next-line unicorn/explicit-length-check const length = ctx.length ? ctx.length.toString() : '-'; - const date = formatCommonAccessLogDate(new Date()); + const date = toCommonAccessLogDateFormat(new Date()); stream.write( util.format( diff --git a/test/format.spec.js b/test/format.spec.js index 5b7d329..9c43d52 100644 --- a/test/format.spec.js +++ b/test/format.spec.js @@ -1,11 +1,11 @@ const { - formatCommonAccessLogDate, - formatOffset, - formatShortMonth, - formatTwoDigits, + toCommonAccessLogDateFormat, + toOffset, + toShortMonth, + toTwoDigits, } = require('../format') -describe('formatTwoDigits', () => { +describe('toTwoDigits', () => { test.each([ { value: 0, expected: '00' }, { value: 1, expected: '01' }, @@ -13,53 +13,53 @@ describe('formatTwoDigits', () => { { value: 99, expected: '99' }, { value: 123, expected: '123' }, ])('returns $expected for $value', ({ value, expected }) => { - expect(formatTwoDigits(value)).toBe(expected) + expect(toTwoDigits(value)).toBe(expected) }) test('throws TypeError if provided value is Infinity', () => { - expect(() => formatTwoDigits(Infinity)).toThrow(TypeError) + expect(() => toTwoDigits(Infinity)).toThrow(TypeError) }) test('throws TypeError if provided value is NaN', () => { - expect(() => formatTwoDigits(NaN)).toThrow(TypeError) + expect(() => toTwoDigits(NaN)).toThrow(TypeError) }) test('throws TypeError if provided value is a decimal number', () => { - expect(() => formatTwoDigits(0.99)).toThrow(TypeError) + expect(() => toTwoDigits(0.99)).toThrow(TypeError) }) test('throws TypeError if provided value is a string', () => { - expect(() => formatTwoDigits('0')).toThrow(TypeError) + expect(() => toTwoDigits('0')).toThrow(TypeError) }) }) -describe('formatOffset', () => { +describe('toOffset', () => { test.each([ { value: 480, expected: '-0800' }, { value: 0, expected: '+0000' }, { value: -180, expected: '+0300' }, ])('returns $expected for $value', ({ value, expected }) => { - expect(formatOffset(value)).toBe(expected) + expect(toOffset(value)).toBe(expected) }) test('throws TypeError if provided value is Infinity', () => { - expect(() => formatOffset(Infinity)).toThrow(TypeError) + expect(() => toOffset(Infinity)).toThrow(TypeError) }) test('throws TypeError if provided value is NaN', () => { - expect(() => formatOffset(NaN)).toThrow(TypeError) + expect(() => toOffset(NaN)).toThrow(TypeError) }) test('throws TypeError if provided value is a decimal number', () => { - expect(() => formatOffset(60.5)).toThrow(TypeError) + expect(() => toOffset(60.5)).toThrow(TypeError) }) test('throws TypeError if provided value is a string', () => { - expect(() => formatOffset('60')).toThrow(TypeError) + expect(() => toOffset('60')).toThrow(TypeError) }) }) -describe('formatShortMonth', () => { +describe('toShortMonth', () => { test.each([ { value: 0, expected: 'Jan' }, { value: 1, expected: 'Feb' }, @@ -74,22 +74,22 @@ describe('formatShortMonth', () => { { value: 10, expected: 'Nov' }, { value: 11, expected: 'Dec' }, ])('returns $expected for $value', ({ value, expected }) => { - expect(formatShortMonth(value)).toBe(expected) + expect(toShortMonth(value)).toBe(expected) }) test('throws TypeError for a non-valid month number', () => { - expect(() => formatShortMonth(13)).toThrow(TypeError) + expect(() => toShortMonth(13)).toThrow(TypeError) }) }) -describe('formatCommonAccessLogDate', () => { +describe('toCommonAccessLogDateFormat', () => { test('correctly formats a date', () => { const date = new Date('2020-01-01T12:34:56Z') const expectedValue = '01/Jan/2020:10:34:56 +0200' - expect(formatCommonAccessLogDate(date)).toBe(expectedValue) + expect(toCommonAccessLogDateFormat(date)).toBe(expectedValue) }) test('throws TypeError for non-Date value', () => { - expect(() => formatCommonAccessLogDate({})).toThrow(TypeError) + expect(() => toCommonAccessLogDateFormat({})).toThrow(TypeError) }) }) \ No newline at end of file From e4c9f81c9f6ba5c35a9efc373c478d2b2c5c9577 Mon Sep 17 00:00:00 2001 From: Souler Date: Sat, 12 Oct 2024 13:56:02 +0200 Subject: [PATCH 3/5] fix: remove unnecessary variable on format/toTwoDigits --- format.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/format.js b/format.js index 44922c6..4ac83c6 100644 --- a/format.js +++ b/format.js @@ -14,9 +14,7 @@ function toTwoDigits(number) { throw new TypeError(`Not a valid integer: ${number}`); } - const n = String(number); - - return n.padStart(2, '0'); + return number.toString().padStart(2, '0'); } /** From 38c09d29081380b5c2cfb5734123b69db574111f Mon Sep 17 00:00:00 2001 From: Souler Date: Sat, 12 Oct 2024 15:25:30 +0200 Subject: [PATCH 4/5] refactor: use look-up object instead of switch-case for months --- format.js | 53 +++++++++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/format.js b/format.js index 4ac83c6..e53223f 100644 --- a/format.js +++ b/format.js @@ -17,43 +17,40 @@ function toTwoDigits(number) { return number.toString().padStart(2, '0'); } +/** + * Look-up map of month number into month short name (in english). + * A month number is the value returned by Date#getMonth() and a short month name + * is a 3 letter representation of a month of the year. + */ +const shortMonthByMonthNumber = { + 0: 'Jan', + 1: 'Feb', + 2: 'Mar', + 3: 'Apr', + 4: 'May', + 5: 'Jun', + 6: 'Jul', + 7: 'Aug', + 8: 'Sep', + 9: 'Oct', + 10: 'Nov', + 11: 'Dec' +}; + /** * Returns a [short](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#month) * (3-letters) representation of the given month index **in English**. + * * @param {number} month * @returns {string} * @throws {TypeError} if the provided value is not a valid month number */ function toShortMonth(month) { - switch (month) { - // Note: January is month 0! - case 0: - return 'Jan'; - case 1: - return 'Feb'; - case 2: - return 'Mar'; - case 3: - return 'Apr'; - case 4: - return 'May'; - case 5: - return 'Jun'; - case 6: - return 'Jul'; - case 7: - return 'Aug'; - case 8: - return 'Sep'; - case 9: - return 'Oct'; - case 10: - return 'Nov'; - case 11: - return 'Dec'; - default: - throw new TypeError(`Not a valid month value: ${month}`); + if (month in shortMonthByMonthNumber) { + throw new TypeError(`Not a valid month value: ${month}`); } + + return shortMonthByMonthNumber[month]; } /** From cc781f2e22a10c1526fa01b09dadba61511b59c6 Mon Sep 17 00:00:00 2001 From: Souler Date: Sat, 12 Oct 2024 17:45:27 +0200 Subject: [PATCH 5/5] fix: wrong condition in toShortMonth --- format.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/format.js b/format.js index e53223f..7c41960 100644 --- a/format.js +++ b/format.js @@ -46,7 +46,7 @@ const shortMonthByMonthNumber = { * @throws {TypeError} if the provided value is not a valid month number */ function toShortMonth(month) { - if (month in shortMonthByMonthNumber) { + if (!(month in shortMonthByMonthNumber)) { throw new TypeError(`Not a valid month value: ${month}`); }