From 975051232592eea1ff1efe37beedbb4e72de01ac Mon Sep 17 00:00:00 2001 From: Akiomi Kamakura Date: Wed, 15 Dec 2021 17:24:28 +0900 Subject: [PATCH 1/2] Add LqWithOffset and LyWithOffset --- src/fiscal-periods/lq-with-offset.test.ts | 19 +++++++++++++++++++ src/fiscal-periods/lq-with-offset.ts | 22 ++++++++++++++++++++++ src/fiscal-periods/ly-with-offset.test.ts | 19 +++++++++++++++++++ src/fiscal-periods/ly-with-offset.ts | 22 ++++++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 src/fiscal-periods/lq-with-offset.test.ts create mode 100644 src/fiscal-periods/lq-with-offset.ts create mode 100644 src/fiscal-periods/ly-with-offset.test.ts create mode 100644 src/fiscal-periods/ly-with-offset.ts diff --git a/src/fiscal-periods/lq-with-offset.test.ts b/src/fiscal-periods/lq-with-offset.test.ts new file mode 100644 index 0000000..f6975e2 --- /dev/null +++ b/src/fiscal-periods/lq-with-offset.test.ts @@ -0,0 +1,19 @@ +import { InvalidLYLQError, ParseError } from '~/fiscal-periods/error' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' + +test('constructor', () => { + expect(() => new LqWithOffset(-1)).not.toThrow(Error) + expect(() => new LqWithOffset(0)).not.toThrow(Error) + expect(() => new LqWithOffset(1)).toThrow(InvalidLYLQError) +}) + +test('parse', () => { + expect(() => LqWithOffset.parse('lq')).not.toThrow(Error) + expect(() => LqWithOffset.parse('LQ')).not.toThrow(Error) + expect(() => LqWithOffset.parse('LQ-1')).not.toThrow(Error) + expect(() => LqWithOffset.parse('LQ-0')).toThrow(ParseError) + expect(() => LqWithOffset.parse('LQ0')).toThrow(ParseError) + expect(() => LqWithOffset.parse('LQ+0')).toThrow(ParseError) + expect(() => LqWithOffset.parse('LQ+1')).toThrow(ParseError) + expect(() => LqWithOffset.parse('foo')).toThrow(ParseError) +}) diff --git a/src/fiscal-periods/lq-with-offset.ts b/src/fiscal-periods/lq-with-offset.ts new file mode 100644 index 0000000..748cf2b --- /dev/null +++ b/src/fiscal-periods/lq-with-offset.ts @@ -0,0 +1,22 @@ +import { InvalidLYLQError, ParseError } from '~/fiscal-periods/error' + +export class LqWithOffset { + constructor(public offset: number = 0) { + if (offset > 0) { + throw new InvalidLYLQError( + `LQ offset must be negative but ${offset} given.` + ) + } + } + + static parse(str: string): LqWithOffset { + str = str.toUpperCase() + const matches = str.match(/^LQ(-[1-9]\d*)?$/) + if (matches == undefined) { + throw new ParseError(`Invalid LQ format: ${str}`) + } + + const offset = str === 'LQ' ? 0 : parseInt(matches[1], 10) + return new LqWithOffset(offset) + } +} diff --git a/src/fiscal-periods/ly-with-offset.test.ts b/src/fiscal-periods/ly-with-offset.test.ts new file mode 100644 index 0000000..7330c98 --- /dev/null +++ b/src/fiscal-periods/ly-with-offset.test.ts @@ -0,0 +1,19 @@ +import { InvalidLYLQError, ParseError } from '~/fiscal-periods/error' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' + +test('constructor', () => { + expect(() => new LyWithOffset(-1)).not.toThrow(Error) + expect(() => new LyWithOffset(0)).not.toThrow(Error) + expect(() => new LyWithOffset(1)).toThrow(InvalidLYLQError) +}) + +test('parse', () => { + expect(() => LyWithOffset.parse('ly')).not.toThrow(Error) + expect(() => LyWithOffset.parse('LY')).not.toThrow(Error) + expect(() => LyWithOffset.parse('LY-1')).not.toThrow(Error) + expect(() => LyWithOffset.parse('LY-0')).toThrow(ParseError) + expect(() => LyWithOffset.parse('LY0')).toThrow(ParseError) + expect(() => LyWithOffset.parse('LY+0')).toThrow(ParseError) + expect(() => LyWithOffset.parse('LY+1')).toThrow(ParseError) + expect(() => LyWithOffset.parse('foo')).toThrow(ParseError) +}) diff --git a/src/fiscal-periods/ly-with-offset.ts b/src/fiscal-periods/ly-with-offset.ts new file mode 100644 index 0000000..29c759a --- /dev/null +++ b/src/fiscal-periods/ly-with-offset.ts @@ -0,0 +1,22 @@ +import { InvalidLYLQError, ParseError } from '~/fiscal-periods/error' + +export class LyWithOffset { + constructor(public offset: number = 0) { + if (offset > 0) { + throw new InvalidLYLQError( + `LY offset must be negative but ${offset} given.` + ) + } + } + + static parse(str: string): LyWithOffset { + str = str.toUpperCase() + const matches = str.match(/^LY(-[1-9]\d*)?$/) + if (matches == undefined) { + throw new ParseError(`Invalid LY format: ${str}`) + } + + const offset = str === 'LY' ? 0 : parseInt(matches[1], 10) + return new LyWithOffset(offset) + } +} From 60af13e0e5b67cb54adb78bbbebd4e4ed1125dac Mon Sep 17 00:00:00 2001 From: Akiomi Kamakura Date: Wed, 15 Dec 2021 18:01:14 +0900 Subject: [PATCH 2/2] Add relative LY LQ support --- src/api/company-service.test.ts | 67 ++++++++++++++++--- src/api/company-service.ts | 18 +++-- src/api/v2/caching-client.test.ts | 9 ++- src/api/v3/caching-client.test.ts | 9 ++- src/api/v3/client.test.ts | 26 ++++++- src/custom-functions/bcode.test.ts | 3 + src/custom-functions/bcode.ts | 7 +- src/custom-functions/v2/bcode-quarter.test.ts | 6 +- src/custom-functions/v2/bcode-quarter.ts | 6 +- src/custom-functions/v2/bcode.ts | 19 ++++-- src/custom-functions/v3/bcode.ts | 2 +- src/fiscal-periods/lq-with-offset.test.ts | 16 +++++ src/fiscal-periods/lq-with-offset.ts | 14 +++- src/fiscal-periods/ly-with-offset.test.ts | 16 +++++ src/fiscal-periods/ly-with-offset.ts | 14 +++- src/fiscal-periods/period-parser.test.ts | 21 +++++- src/fiscal-periods/year-quarter-param.test.ts | 49 +++++++------- src/fiscal-periods/year-quarter-param.ts | 31 ++++++--- 18 files changed, 267 insertions(+), 66 deletions(-) diff --git a/src/api/company-service.test.ts b/src/api/company-service.test.ts index 636d07e..4e54137 100644 --- a/src/api/company-service.test.ts +++ b/src/api/company-service.test.ts @@ -2,9 +2,14 @@ import { CompanyService } from '~/api/company-service' import { CachingBuffettCodeApiClientV2 } from '~/api/v2/caching-client' import { CachingBuffettCodeApiClientV3 } from '~/api/v3/caching-client' import { DateParam } from '~/fiscal-periods/date-param' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' import { YearQuarter } from '~/fiscal-periods/year-quarter' import { YearQuarterParam } from '~/fiscal-periods/year-quarter-param' +const LY = new LyWithOffset() +const LQ = new LqWithOffset() + jest.mock('~/api/v2/client', () => jest.requireActual('~/__mocks__/api/v2/client') ) @@ -25,18 +30,49 @@ test('convertYearQuarterParamToYearQuarter', () => { const client = new CachingBuffettCodeApiClientV2('token') const service = new CompanyService('2371', client) expect( - service.convertYearQuarterParamToYearQuarter(new YearQuarterParam('LY', 3)) + service.convertYearQuarterParamToYearQuarter(new YearQuarterParam(LY, 3)) ).toEqual(new YearQuarter(2021, 3)) + expect( + service.convertYearQuarterParamToYearQuarter(new YearQuarterParam(2016, LQ)) + ).toEqual(new YearQuarter(2016, 2)) + expect( + service.convertYearQuarterParamToYearQuarter(new YearQuarterParam(LY, LQ)) + ).toEqual(new YearQuarter(2021, 2)) expect( service.convertYearQuarterParamToYearQuarter( - new YearQuarterParam(2016, 'LQ') + new YearQuarterParam(new LyWithOffset(-5), LQ) ) ).toEqual(new YearQuarter(2016, 2)) expect( service.convertYearQuarterParamToYearQuarter( - new YearQuarterParam('LY', 'LQ') + new YearQuarterParam(LY, new LqWithOffset(-1)) ) - ).toEqual(new YearQuarter(2021, 2)) + ).toEqual(new YearQuarter(2021, 1)) + expect( + service.convertYearQuarterParamToYearQuarter( + new YearQuarterParam(LY, new LqWithOffset(-2)) + ) + ).toEqual(new YearQuarter(2020, 4)) + expect( + service.convertYearQuarterParamToYearQuarter( + new YearQuarterParam(LY, new LqWithOffset(-3)) + ) + ).toEqual(new YearQuarter(2020, 3)) + expect( + service.convertYearQuarterParamToYearQuarter( + new YearQuarterParam(LY, new LqWithOffset(-4)) + ) + ).toEqual(new YearQuarter(2020, 2)) + expect( + service.convertYearQuarterParamToYearQuarter( + new YearQuarterParam(LY, new LqWithOffset(-5)) + ) + ).toEqual(new YearQuarter(2020, 1)) + expect( + service.convertYearQuarterParamToYearQuarter( + new YearQuarterParam(new LyWithOffset(-5), new LqWithOffset(-5)) + ) + ).toEqual(new YearQuarter(2015, 1)) }) test('isOndemandQuarterApiPeriod', () => { @@ -52,15 +88,30 @@ test('isOndemandQuarterApiPeriod', () => { expect(service.isOndemandQuarterApiPeriod(new YearQuarter(2016, 3))).toBe( false ) + expect(service.isOndemandQuarterApiPeriod(new YearQuarterParam(LY, 3))).toBe( + false + ) + expect( + service.isOndemandQuarterApiPeriod(new YearQuarterParam(2016, LQ)) + ).toBe(true) + expect(service.isOndemandQuarterApiPeriod(new YearQuarterParam(LY, LQ))).toBe( + false + ) expect( - service.isOndemandQuarterApiPeriod(new YearQuarterParam('LY', 3)) + service.isOndemandQuarterApiPeriod( + new YearQuarterParam(new LyWithOffset(-4), new LqWithOffset(-3)) + ) ).toBe(false) expect( - service.isOndemandQuarterApiPeriod(new YearQuarterParam(2016, 'LQ')) + service.isOndemandQuarterApiPeriod( + new YearQuarterParam(new LyWithOffset(-4), new LqWithOffset(-4)) + ) ).toBe(true) expect( - service.isOndemandQuarterApiPeriod(new YearQuarterParam('LY', 'LQ')) - ).toBe(false) + service.isOndemandQuarterApiPeriod( + new YearQuarterParam(new LyWithOffset(-5), LQ) + ) + ).toBe(true) }) test('isOndemandDailyApiPeriod', () => { diff --git a/src/api/company-service.ts b/src/api/company-service.ts index e4853aa..9d27c9f 100644 --- a/src/api/company-service.ts +++ b/src/api/company-service.ts @@ -1,6 +1,8 @@ import { BuffettCodeApiClientV2 } from '~/api/v2/client' import { BuffettCodeApiClientV3 } from '~/api/v3/client' import { DateParam } from '~/fiscal-periods/date-param' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' import { YearQuarter } from '~/fiscal-periods/year-quarter' import { YearQuarterParam } from '~/fiscal-periods/year-quarter-param' @@ -22,12 +24,20 @@ export class CompanyService { public convertYearQuarterParamToYearQuarter( period: YearQuarterParam ): YearQuarter { - if (period.isLatestYear()) { - period.year = this.company['latest_fiscal_year'] + if (period.year instanceof LyWithOffset) { + period.year = this.company['latest_fiscal_year'] + period.year.offset } - if (period.isLatestQuarter()) { - period.quarter = this.company['latest_fiscal_quarter'] + if (period.quarter instanceof LqWithOffset) { + period.year = + (period.year as number) + Math.ceil(period.quarter.offset / 4) + period.quarter = + this.company['latest_fiscal_quarter'] + (period.quarter.offset % 4) + + if (period.quarter <= 0) { + period.year -= 1 + period.quarter = 4 + (period.quarter as number) + } } return period.toYearQuarter() diff --git a/src/api/v2/caching-client.test.ts b/src/api/v2/caching-client.test.ts index 99bc879..1d8f180 100644 --- a/src/api/v2/caching-client.test.ts +++ b/src/api/v2/caching-client.test.ts @@ -1,6 +1,8 @@ import { CompanyCache } from '~/__mocks__/services/company-cache' import { QuarterCache } from '~/__mocks__/services/quarter-cache' import { CachingBuffettCodeApiClientV2 } from '~/api/v2/caching-client' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' import { YearQuarter } from '~/fiscal-periods/year-quarter' import { YearQuarterParam } from '~/fiscal-periods/year-quarter-param' import { IndicatorCache } from '~/services/indicator-cache' @@ -18,6 +20,9 @@ jest.mock('~/services/quarter-cache', () => jest.requireActual('~/__mocks__/services/quarter-cache') ) +const LY = new LyWithOffset() +const LQ = new LqWithOffset() + describe('company', () => { const ticker = '2371' @@ -111,7 +116,7 @@ describe('quarter', () => { QuarterCache.clearAll() }) - const period = new YearQuarterParam('LY', 'LQ') + const period = new YearQuarterParam(LY, LQ) test('(uncached)', () => { expect(QuarterCache.getData(ticker, new YearQuarter(2018, 1))).toBeNull() @@ -168,7 +173,7 @@ describe('ondemandQuarter', () => { QuarterCache.clearAll() }) - const period = new YearQuarterParam('LY', 'LQ') + const period = new YearQuarterParam(LY, LQ) test('(uncached)', () => { expect(QuarterCache.getData(ticker, new YearQuarter(2018, 1))).toBeNull() diff --git a/src/api/v3/caching-client.test.ts b/src/api/v3/caching-client.test.ts index c9c77df..47e3a09 100644 --- a/src/api/v3/caching-client.test.ts +++ b/src/api/v3/caching-client.test.ts @@ -3,6 +3,8 @@ import { DailyCache } from '~/__mocks__/services/daily-cache' import { QuarterCache } from '~/__mocks__/services/quarter-cache' import { CachingBuffettCodeApiClientV3 } from '~/api/v3/caching-client' import { DateParam } from '~/fiscal-periods/date-param' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' import { YearQuarter } from '~/fiscal-periods/year-quarter' import { YearQuarterParam } from '~/fiscal-periods/year-quarter-param' @@ -19,6 +21,9 @@ jest.mock('~/services/quarter-cache', () => jest.requireActual('~/__mocks__/services/quarter-cache') ) +const LY = new LyWithOffset() +const LQ = new LqWithOffset() + describe('company', () => { const ticker = '2371' @@ -112,7 +117,7 @@ describe('quarter', () => { QuarterCache.clearAll() }) - const period = new YearQuarterParam('LY', 'LQ') + const period = new YearQuarterParam(LY, LQ) test('(uncached)', () => { expect(QuarterCache.get(ticker, new YearQuarter(2018, 1))).toBeNull() @@ -165,7 +170,7 @@ describe('ondemandQuarter', () => { QuarterCache.clearAll() }) - const period = new YearQuarterParam('LY', 'LQ') + const period = new YearQuarterParam(LY, LQ) test('(uncached)', () => { expect(QuarterCache.get(ticker, new YearQuarter(2018, 1))).toBeNull() diff --git a/src/api/v3/client.test.ts b/src/api/v3/client.test.ts index 91cda2e..470cf76 100644 --- a/src/api/v3/client.test.ts +++ b/src/api/v3/client.test.ts @@ -8,6 +8,8 @@ import { Daily } from '~/entities/v3/daily' import { Quarter } from '~/entities/v3/quarter' import { DateParam } from '~/fiscal-periods/date-param' import { DateRange } from '~/fiscal-periods/date-range' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' import { YearQuarter } from '~/fiscal-periods/year-quarter' import { YearQuarterParam } from '~/fiscal-periods/year-quarter-param' import { YearQuarterRange } from '~/fiscal-periods/year-quarter-range' @@ -97,7 +99,7 @@ describe('BuffettCodeApiClientV3', () => { }) }) - test('quarter', () => { + test('quarter (FY/FQ)', () => { const mockFetch = useMockedUrlFetchApp(200, JSON.stringify(quarter)) const client = new BuffettCodeApiClientV3('foo') @@ -117,6 +119,28 @@ describe('BuffettCodeApiClientV3', () => { }) }) + test('quarter (LY/LQ)', () => { + const mockFetch = useMockedUrlFetchApp(200, JSON.stringify(quarter)) + + const LY = new LyWithOffset() + const LQ = new LqWithOffset() + const client = new BuffettCodeApiClientV3('foo') + const ticker = '2371' + const period = new YearQuarterParam(LY, LQ) + expect(client.quarter(ticker, period)).toEqual( + Quarter.fromResponse(quarter) + ) + expect(mockFetch.mock.calls.length).toBe(1) + expect(mockFetch.mock.calls[0].length).toBe(2) + expect(mockFetch.mock.calls[0][0]).toBe( + 'https://api.buffett-code.com/api/v3/quarter?ticker=2371&fy=LY&fq=LQ' + ) + expect(mockFetch.mock.calls[0][1]).toEqual({ + headers: { 'x-api-key': 'foo' }, + muteHttpExceptions: true + }) + }) + test('bulkQuarter', () => { const bulkQuarter = { data: { diff --git a/src/custom-functions/bcode.test.ts b/src/custom-functions/bcode.test.ts index 78798a6..8c1c7a8 100644 --- a/src/custom-functions/bcode.test.ts +++ b/src/custom-functions/bcode.test.ts @@ -23,6 +23,9 @@ test('isV3Call', () => { expect(isV3Call('ly', '4')).toBeFalsy() expect(isV3Call('2020', 'LQ')).toBeFalsy() expect(isV3Call('2020', 'lq')).toBeFalsy() + expect(isV3Call('LY', 'LQ')).toBeFalsy() + expect(isV3Call('LY-1', 'LQ-1')).toBeFalsy() expect(isV3Call('', '')).toBeFalsy() expect(isV3Call('2020Q1', 'net_sales')).toBeTruthy() + expect(isV3Call('LYLQ', 'net_sales')).toBeTruthy() }) diff --git a/src/custom-functions/bcode.ts b/src/custom-functions/bcode.ts index 6246977..c6f5b1c 100644 --- a/src/custom-functions/bcode.ts +++ b/src/custom-functions/bcode.ts @@ -1,5 +1,7 @@ import { bcode as bcodeV2 } from '~/custom-functions/v2/bcode' import { bcode as bcodeV3 } from '~/custom-functions/v3/bcode' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' export function castStringAsBoolean(bool: string | boolean): boolean { return typeof bool === 'string' ? bool.toLowerCase() === 'true' : bool @@ -15,7 +17,10 @@ export function isV3Call( return false } else if (param1 === '' || param2 === '') { return false - } else if (param1.toUpperCase() === 'LY' || param2.toUpperCase() === 'LQ') { + } else if ( + LyWithOffset.isValidFormat(param1) || + LqWithOffset.isValidFormat(param2) + ) { return false } else if (param1.match(/^\d+$/) || param1.match(/^\d+$/)) { return false diff --git a/src/custom-functions/v2/bcode-quarter.test.ts b/src/custom-functions/v2/bcode-quarter.test.ts index 592109f..011c8ef 100644 --- a/src/custom-functions/v2/bcode-quarter.test.ts +++ b/src/custom-functions/v2/bcode-quarter.test.ts @@ -2,6 +2,8 @@ import { QuarterCache } from '~/__mocks__/services/quarter-cache' import { CachingBuffettCodeApiClientV2 } from '~/api/v2/caching-client' import { bcodeQuarter } from '~/custom-functions/v2/bcode-quarter' import { BcodeResult } from '~/custom-functions/v2/bcode-result' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' import { YearQuarter } from '~/fiscal-periods/year-quarter' import { QuarterPropertyCache } from '~/services/quarter-property-cache' @@ -45,13 +47,15 @@ describe('bcodeQuarter', () => { }) test('(quarter, LY, LQ)', () => { + const LY = new LyWithOffset() + const LQ = new LqWithOffset() const ticker = '2371' const period = new YearQuarter(2018, 1) expect(QuarterCache.getData(ticker, period)).toBeNull() expect(QuarterPropertyCache.get()).toBeNull() const client = new CachingBuffettCodeApiClientV2('token') - const result = bcodeQuarter(client, ticker, 'LY', 'LQ', 'net_sales', false) + const result = bcodeQuarter(client, ticker, LY, LQ, 'net_sales', false) expect(result).toEqual(new BcodeResult(12513000000.0, '百万円')) expect(QuarterCache.getData(ticker, period)['net_sales']).toBe( diff --git a/src/custom-functions/v2/bcode-quarter.ts b/src/custom-functions/v2/bcode-quarter.ts index fe37a19..691ab26 100644 --- a/src/custom-functions/v2/bcode-quarter.ts +++ b/src/custom-functions/v2/bcode-quarter.ts @@ -7,13 +7,15 @@ import { UnsupportedTickerError } from '~/custom-functions/error' import { BcodeResult } from '~/custom-functions/v2/bcode-result' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' import { YearQuarterParam } from '~/fiscal-periods/year-quarter-param' export function bcodeQuarter( client: CachingBuffettCodeApiClientV2, ticker: string, - fiscalYear: number | 'LY', - fiscalQuarter: number | 'LQ', + fiscalYear: number | LyWithOffset, + fiscalQuarter: number | LqWithOffset, propertyName: string, ondemandApiEnabled: boolean ): BcodeResult { diff --git a/src/custom-functions/v2/bcode.ts b/src/custom-functions/v2/bcode.ts index ccc2f40..2213212 100644 --- a/src/custom-functions/v2/bcode.ts +++ b/src/custom-functions/v2/bcode.ts @@ -15,6 +15,8 @@ import { InvalidYearError, InvalidQuarterError } from '~/fiscal-periods/error' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' import { Setting } from '~/setting' function validate( @@ -42,7 +44,6 @@ function validate( } // TODO: エラーハンドリングの改善 -// TODO: fiscalYearとfiscalQuarterの型をstringではなく'LY'と'LQ'に変更する export function bcode( ticker: string, fiscalYear: string | number, @@ -68,13 +69,21 @@ export function bcode( try { let result: BcodeResult if (fiscalYear && fiscalQuarter) { + const fy = + typeof fiscalYear === 'string' && fiscalYear.substring(0, 2) === 'LY' + ? LyWithOffset.parse(fiscalYear) + : parseInt(fiscalYear.toString(), 10) + const fq = + typeof fiscalQuarter === 'string' && + fiscalQuarter.substring(0, 2) === 'LQ' + ? LqWithOffset.parse(fiscalQuarter) + : parseInt(fiscalQuarter.toString(), 10) + result = bcodeQuarter( client, ticker, - fiscalYear === 'LY' ? fiscalYear : parseInt(fiscalYear.toString(), 10), - fiscalQuarter === 'LQ' - ? fiscalQuarter - : parseInt(fiscalQuarter.toString(), 10), + fy, + fq, propertyName, setting.ondemandApiEnabled ) diff --git a/src/custom-functions/v3/bcode.ts b/src/custom-functions/v3/bcode.ts index 1c0b9b5..0f635fe 100644 --- a/src/custom-functions/v3/bcode.ts +++ b/src/custom-functions/v3/bcode.ts @@ -44,7 +44,7 @@ function handleError(e): void { ) } } else if (e instanceof InvalidLYLQError) { - throw new Error('<>') + throw new Error('<<無効なLY・LQが指定されています>>') } else if (e instanceof InvalidYearError) { throw new Error(`<<無効な決算年度が指定されています>>`) } else if (e instanceof InvalidQuarterError) { diff --git a/src/fiscal-periods/lq-with-offset.test.ts b/src/fiscal-periods/lq-with-offset.test.ts index f6975e2..dcce539 100644 --- a/src/fiscal-periods/lq-with-offset.test.ts +++ b/src/fiscal-periods/lq-with-offset.test.ts @@ -7,6 +7,17 @@ test('constructor', () => { expect(() => new LqWithOffset(1)).toThrow(InvalidLYLQError) }) +test('isValidFormat', () => { + expect(LqWithOffset.isValidFormat('lq')).toBeTruthy() + expect(LqWithOffset.isValidFormat('LQ')).toBeTruthy() + expect(LqWithOffset.isValidFormat('LQ-1')).toBeTruthy() + expect(LqWithOffset.isValidFormat('LQ-0')).toBeFalsy() + expect(LqWithOffset.isValidFormat('LQ0')).toBeFalsy() + expect(LqWithOffset.isValidFormat('LQ+0')).toBeFalsy() + expect(LqWithOffset.isValidFormat('LQ+1')).toBeFalsy() + expect(LqWithOffset.isValidFormat('foo')).toBeFalsy() +}) + test('parse', () => { expect(() => LqWithOffset.parse('lq')).not.toThrow(Error) expect(() => LqWithOffset.parse('LQ')).not.toThrow(Error) @@ -17,3 +28,8 @@ test('parse', () => { expect(() => LqWithOffset.parse('LQ+1')).toThrow(ParseError) expect(() => LqWithOffset.parse('foo')).toThrow(ParseError) }) + +test('toString', () => { + expect(new LqWithOffset().toString()).toEqual('LQ') + expect(new LqWithOffset(-1).toString()).toEqual('LQ-1') +}) diff --git a/src/fiscal-periods/lq-with-offset.ts b/src/fiscal-periods/lq-with-offset.ts index 748cf2b..fd1c612 100644 --- a/src/fiscal-periods/lq-with-offset.ts +++ b/src/fiscal-periods/lq-with-offset.ts @@ -1,6 +1,8 @@ import { InvalidLYLQError, ParseError } from '~/fiscal-periods/error' export class LqWithOffset { + private static pattern = /^LQ(-[1-9]\d*)?$/ + constructor(public offset: number = 0) { if (offset > 0) { throw new InvalidLYLQError( @@ -9,9 +11,15 @@ export class LqWithOffset { } } + static isValidFormat(str: string): boolean { + str = str.toUpperCase() + const matches = str.match(this.pattern) + return matches != undefined + } + static parse(str: string): LqWithOffset { str = str.toUpperCase() - const matches = str.match(/^LQ(-[1-9]\d*)?$/) + const matches = str.match(this.pattern) if (matches == undefined) { throw new ParseError(`Invalid LQ format: ${str}`) } @@ -19,4 +27,8 @@ export class LqWithOffset { const offset = str === 'LQ' ? 0 : parseInt(matches[1], 10) return new LqWithOffset(offset) } + + toString(): string { + return this.offset === 0 ? 'LQ' : `LQ${this.offset}` + } } diff --git a/src/fiscal-periods/ly-with-offset.test.ts b/src/fiscal-periods/ly-with-offset.test.ts index 7330c98..b9b4727 100644 --- a/src/fiscal-periods/ly-with-offset.test.ts +++ b/src/fiscal-periods/ly-with-offset.test.ts @@ -7,6 +7,17 @@ test('constructor', () => { expect(() => new LyWithOffset(1)).toThrow(InvalidLYLQError) }) +test('isValidFormat', () => { + expect(LyWithOffset.isValidFormat('ly')).toBeTruthy() + expect(LyWithOffset.isValidFormat('LY')).toBeTruthy() + expect(LyWithOffset.isValidFormat('LY-1')).toBeTruthy() + expect(LyWithOffset.isValidFormat('LY-0')).toBeFalsy() + expect(LyWithOffset.isValidFormat('LY0')).toBeFalsy() + expect(LyWithOffset.isValidFormat('LY+0')).toBeFalsy() + expect(LyWithOffset.isValidFormat('LY+1')).toBeFalsy() + expect(LyWithOffset.isValidFormat('foo')).toBeFalsy() +}) + test('parse', () => { expect(() => LyWithOffset.parse('ly')).not.toThrow(Error) expect(() => LyWithOffset.parse('LY')).not.toThrow(Error) @@ -17,3 +28,8 @@ test('parse', () => { expect(() => LyWithOffset.parse('LY+1')).toThrow(ParseError) expect(() => LyWithOffset.parse('foo')).toThrow(ParseError) }) + +test('toString', () => { + expect(new LyWithOffset().toString()).toEqual('LY') + expect(new LyWithOffset(-1).toString()).toEqual('LY-1') +}) diff --git a/src/fiscal-periods/ly-with-offset.ts b/src/fiscal-periods/ly-with-offset.ts index 29c759a..d3979b4 100644 --- a/src/fiscal-periods/ly-with-offset.ts +++ b/src/fiscal-periods/ly-with-offset.ts @@ -1,6 +1,8 @@ import { InvalidLYLQError, ParseError } from '~/fiscal-periods/error' export class LyWithOffset { + private static pattern = /^LY(-[1-9]\d*)?$/ + constructor(public offset: number = 0) { if (offset > 0) { throw new InvalidLYLQError( @@ -9,9 +11,15 @@ export class LyWithOffset { } } + static isValidFormat(str: string): boolean { + str = str.toUpperCase() + const matches = str.match(this.pattern) + return matches != undefined + } + static parse(str: string): LyWithOffset { str = str.toUpperCase() - const matches = str.match(/^LY(-[1-9]\d*)?$/) + const matches = str.match(this.pattern) if (matches == undefined) { throw new ParseError(`Invalid LY format: ${str}`) } @@ -19,4 +27,8 @@ export class LyWithOffset { const offset = str === 'LY' ? 0 : parseInt(matches[1], 10) return new LyWithOffset(offset) } + + toString(): string { + return this.offset === 0 ? 'LY' : `LY${this.offset}` + } } diff --git a/src/fiscal-periods/period-parser.test.ts b/src/fiscal-periods/period-parser.test.ts index b480a31..412828e 100644 --- a/src/fiscal-periods/period-parser.test.ts +++ b/src/fiscal-periods/period-parser.test.ts @@ -1,17 +1,32 @@ import { DateParam } from '~/fiscal-periods/date-param' import { ParseError } from '~/fiscal-periods/error' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' import { PeriodParser } from '~/fiscal-periods/period-parser' import { YearQuarterParam } from '~/fiscal-periods/year-quarter-param' +const LY = new LyWithOffset() +const LQ = new LqWithOffset() + test('parse', () => { expect(PeriodParser.parse('2020Q3')).toEqual(new YearQuarterParam(2020, 3)) - expect(PeriodParser.parse('2020LQ')).toEqual(new YearQuarterParam(2020, 'LQ')) - expect(PeriodParser.parse('LYQ3')).toEqual(new YearQuarterParam('LY', 3)) - expect(PeriodParser.parse('LYLQ')).toEqual(new YearQuarterParam('LY', 'LQ')) + expect(PeriodParser.parse('2020LQ')).toEqual(new YearQuarterParam(2020, LQ)) + expect(PeriodParser.parse('LYQ3')).toEqual(new YearQuarterParam(LY, 3)) + expect(PeriodParser.parse('LYLQ')).toEqual(new YearQuarterParam(LY, LQ)) + expect(PeriodParser.parse('LY-1Q4')).toEqual( + new YearQuarterParam(new LyWithOffset(-1), 4) + ) + expect(PeriodParser.parse('2020LQ-1')).toEqual( + new YearQuarterParam(2020, new LqWithOffset(-1)) + ) + expect(PeriodParser.parse('LY-1LQ-1')).toEqual( + new YearQuarterParam(new LyWithOffset(-1), new LqWithOffset(-1)) + ) expect(PeriodParser.parse('2020-09-06')).toEqual( new DateParam(new Date(2020, 9, 6)) ) expect(() => PeriodParser.parse('foo')).toThrow(ParseError) expect(() => PeriodParser.parse('2020/09/06')).toThrow(ParseError) expect(() => PeriodParser.parse('latest')).toThrow(ParseError) + expect(() => PeriodParser.parse('0Q1')).toThrow(ParseError) }) diff --git a/src/fiscal-periods/year-quarter-param.test.ts b/src/fiscal-periods/year-quarter-param.test.ts index 7282f19..a1268ec 100644 --- a/src/fiscal-periods/year-quarter-param.test.ts +++ b/src/fiscal-periods/year-quarter-param.test.ts @@ -3,9 +3,14 @@ import { InvalidQuarterError, ParseError } from '~/fiscal-periods/error' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' import { YearQuarter } from '~/fiscal-periods/year-quarter' import { YearQuarterParam } from '~/fiscal-periods/year-quarter-param' +const LY = new LyWithOffset() +const LQ = new LqWithOffset() + test('constructor', () => { expect(() => new YearQuarterParam(0, 3)).toThrow(InvalidYearError) expect(() => new YearQuarterParam(1, 3)).not.toThrow(Error) @@ -13,40 +18,38 @@ test('constructor', () => { expect(() => new YearQuarterParam(2018, 1)).not.toThrow(Error) expect(() => new YearQuarterParam(2018, 4)).not.toThrow(Error) expect(() => new YearQuarterParam(2018, 5)).toThrow(InvalidQuarterError) - expect(() => new YearQuarterParam('LY', 0)).toThrow(InvalidQuarterError) - expect(() => new YearQuarterParam('LY', 1)).not.toThrow(Error) - expect(() => new YearQuarterParam('LY', 4)).not.toThrow(Error) - expect(() => new YearQuarterParam('LY', 5)).toThrow(InvalidQuarterError) - expect(() => new YearQuarterParam(0, 'LQ')).toThrow(InvalidYearError) - expect(() => new YearQuarterParam(2018, 'LQ')).not.toThrow(Error) - expect(() => new YearQuarterParam('LY', 'LQ')).not.toThrow(Error) + expect(() => new YearQuarterParam(LY, 0)).toThrow(InvalidQuarterError) + expect(() => new YearQuarterParam(LY, 1)).not.toThrow(Error) + expect(() => new YearQuarterParam(LY, 4)).not.toThrow(Error) + expect(() => new YearQuarterParam(LY, 5)).toThrow(InvalidQuarterError) + expect(() => new YearQuarterParam(0, LQ)).toThrow(InvalidYearError) + expect(() => new YearQuarterParam(2018, LQ)).not.toThrow(Error) + expect(() => new YearQuarterParam(LY, LQ)).not.toThrow(Error) }) test('convertibleToYearQuarter', () => { - expect( - new YearQuarterParam('LY', 'LQ').convertibleToYearQuarter() - ).toBeFalsy() - expect( - new YearQuarterParam(2020, 'LQ').convertibleToYearQuarter() - ).toBeFalsy() - expect(new YearQuarterParam('LY', 3).convertibleToYearQuarter()).toBeFalsy() + expect(new YearQuarterParam(LY, LQ).convertibleToYearQuarter()).toBeFalsy() + expect(new YearQuarterParam(2020, LQ).convertibleToYearQuarter()).toBeFalsy() + expect(new YearQuarterParam(LY, 3).convertibleToYearQuarter()).toBeFalsy() expect(new YearQuarterParam(2020, 3).convertibleToYearQuarter()).toBeTruthy() }) test('toYearQuarter', () => { - expect(() => new YearQuarterParam('LY', 'LQ').toYearQuarter()).toThrow(Error) + expect(() => new YearQuarterParam(LY, 3).toYearQuarter()).toThrow(Error) + expect(() => new YearQuarterParam(2020, LQ).toYearQuarter()).toThrow(Error) + expect(() => new YearQuarterParam(LY, LQ).toYearQuarter()).toThrow(Error) expect(new YearQuarterParam(2020, 3).toYearQuarter()).toEqual( new YearQuarter(2020, 3) ) }) test('isLatestYear', () => { - expect(new YearQuarterParam('LY', 3).isLatestYear()).toBeTruthy() + expect(new YearQuarterParam(LY, 3).isLatestYear()).toBeTruthy() expect(new YearQuarterParam(2020, 3).isLatestYear()).toBeFalsy() }) test('isLatestQuarter', () => { - expect(new YearQuarterParam(2020, 'LQ').isLatestQuarter()).toBeTruthy() + expect(new YearQuarterParam(2020, LQ).isLatestQuarter()).toBeTruthy() expect(new YearQuarterParam(2020, 3).isLatestQuarter()).toBeFalsy() }) @@ -64,15 +67,11 @@ test('parse', () => { new YearQuarterParam(2020, 3) ) expect(YearQuarterParam.parse('2020LQ')).toEqual( - new YearQuarterParam(2020, 'LQ') - ) - expect(YearQuarterParam.parse('LYQ3')).toEqual(new YearQuarterParam('LY', 3)) - expect(YearQuarterParam.parse('LYLQ')).toEqual( - new YearQuarterParam('LY', 'LQ') - ) - expect(YearQuarterParam.parse('lylq')).toEqual( - new YearQuarterParam('LY', 'LQ') + new YearQuarterParam(2020, LQ) ) + expect(YearQuarterParam.parse('LYQ3')).toEqual(new YearQuarterParam(LY, 3)) + expect(YearQuarterParam.parse('LYLQ')).toEqual(new YearQuarterParam(LY, LQ)) + expect(YearQuarterParam.parse('lylq')).toEqual(new YearQuarterParam(LY, LQ)) expect(() => YearQuarterParam.parse('20Q3')).toThrow(ParseError) expect(() => YearQuarterParam.parse('foo')).toThrow(ParseError) diff --git a/src/fiscal-periods/year-quarter-param.ts b/src/fiscal-periods/year-quarter-param.ts index 48003e2..f6f54a0 100644 --- a/src/fiscal-periods/year-quarter-param.ts +++ b/src/fiscal-periods/year-quarter-param.ts @@ -3,15 +3,20 @@ import { InvalidQuarterError, ParseError } from '~/fiscal-periods/error' +import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' +import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' import { YearQuarter } from '~/fiscal-periods/year-quarter' export class YearQuarterParam { - constructor(public year: number | 'LY', public quarter: number | 'LQ') { - if (!this.isLatestYear() && year < 1) { + constructor( + public year: number | LyWithOffset, + public quarter: number | LqWithOffset + ) { + if (typeof year === 'number' && year < 1) { throw new InvalidYearError(`Invalid year value: ${year}`) } - if (!this.isLatestQuarter() && (quarter < 1 || quarter > 4)) { + if (typeof quarter === 'number' && (quarter < 1 || quarter > 4)) { throw new InvalidQuarterError(`Invalid quarter value: ${quarter}`) } } @@ -21,7 +26,10 @@ export class YearQuarterParam { } public toYearQuarter(): YearQuarter { - if (this.year === 'LY' || this.quarter === 'LQ') { + if ( + this.year instanceof LyWithOffset || + this.quarter instanceof LqWithOffset + ) { throw new Error('This cannot convert to YearQuarter') } else { return new YearQuarter(this.year, this.quarter) @@ -29,23 +37,28 @@ export class YearQuarterParam { } public isLatestYear(): boolean { - return this.year === 'LY' + return this.year instanceof LyWithOffset } public isLatestQuarter(): boolean { - return this.quarter === 'LQ' + return this.quarter instanceof LqWithOffset } static parse(str: string): YearQuarterParam { str = str.toUpperCase() - const matches = str.match(/^(\d{4}|LY)(Q\d|LQ)$/) + const matches = str.match(/^(\d{4}|LY(-[1-9]\d*)?)(Q\d|LQ(-[1-9]\d*)?)$/) if (matches == undefined) { throw new ParseError(`Invalid year-quarter format: ${str}`) } - const year = matches[1] === 'LY' ? 'LY' : parseInt(matches[1], 10) + const year = + matches[1].substring(0, 2) === 'LY' + ? LyWithOffset.parse(matches[1]) + : parseInt(matches[1], 10) const quarter = - matches[2] === 'LQ' ? 'LQ' : parseInt(matches[2].substring(1), 10) + matches[3].substring(0, 2) === 'LQ' + ? LqWithOffset.parse(matches[3]) + : parseInt(matches[3].substring(1), 10) return new YearQuarterParam(year, quarter) }