diff --git a/README.md b/README.md index 0722f94..03b1094 100644 --- a/README.md +++ b/README.md @@ -64,4 +64,5 @@ npm run deploy:prod 1. Edit `src/version.ts`. 2. Merge development branch into master. 3. Create a release on github. -3. Create Deployments from `Publish` -> `Deploy from manifest` in script editor for prodcution. +4. Run `git checkout master && npm run deploy:prod` to deploy master branch. +5. Create Deployments from `Publish` -> `Deploy from manifest` in script editor for prodcution. diff --git a/src/__mocks__/fixtures/v3/quarter.js b/src/__mocks__/fixtures/v3/quarter.js index 2ea4359..0f3cf09 100644 --- a/src/__mocks__/fixtures/v3/quarter.js +++ b/src/__mocks__/fixtures/v3/quarter.js @@ -182,7 +182,7 @@ module.exports = { employee_num: null, net_sales_per_employee: null, operating_income_per_employee: null, - segment_member: null, + segment_member: {"segments":[],"periods":[],"values":[]}, increase_in_properties: 346000000, r_and_d_expenses: 35000000, defined_benefit_asset: null, diff --git a/src/api/company-service.test.ts b/src/api/company-service.test.ts index 8625d3a..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') ) @@ -21,6 +26,55 @@ test('isSupportedTicker', () => { expect(new CompanyService('9999', client).isSupportedTicker()).toBe(false) }) +test('convertYearQuarterParamToYearQuarter', () => { + const client = new CachingBuffettCodeApiClientV2('token') + const service = new CompanyService('2371', client) + expect( + 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(new LyWithOffset(-5), LQ) + ) + ).toEqual(new YearQuarter(2016, 2)) + expect( + service.convertYearQuarterParamToYearQuarter( + new YearQuarterParam(LY, new LqWithOffset(-1)) + ) + ).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', () => { const client = new CachingBuffettCodeApiClientV2('token') const service = new CompanyService('2371', client) @@ -34,9 +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', 'LQ')) + service.isOndemandQuarterApiPeriod( + new YearQuarterParam(new LyWithOffset(-4), new LqWithOffset(-3)) + ) ).toBe(false) + expect( + service.isOndemandQuarterApiPeriod( + new YearQuarterParam(new LyWithOffset(-4), new LqWithOffset(-4)) + ) + ).toBe(true) + expect( + 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 1c5bbf6..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' @@ -19,6 +21,28 @@ export class CompanyService { return !!this.company } + public convertYearQuarterParamToYearQuarter( + period: YearQuarterParam + ): YearQuarter { + if (period.year instanceof LyWithOffset) { + period.year = this.company['latest_fiscal_year'] + period.year.offset + } + + 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() + } + public isOndemandQuarterApiPeriod( _period: YearQuarter | YearQuarterParam ): boolean { @@ -26,13 +50,9 @@ export class CompanyService { throw new Error('unsupported ticker') } - let period + let period: YearQuarter if (_period instanceof YearQuarterParam) { - if (_period.isLatestYear() && _period.isLatestQuarter()) { - return false - } - - period = _period.toYearQuarter() + period = this.convertYearQuarterParamToYearQuarter(_period) } else { period = _period } diff --git a/src/api/http-error.test.ts b/src/api/http-error.test.ts index 975f98d..190dd6c 100644 --- a/src/api/http-error.test.ts +++ b/src/api/http-error.test.ts @@ -3,6 +3,6 @@ import { useMockedUrlFetchApp } from '~/api/test-helper' test('HttpError', () => { const res = useMockedUrlFetchApp(403, '{"message": "Forbidden"}')() - const error = new HttpError(res) + const error = new HttpError('https://example.com', res) expect(error instanceof HttpError).toBeTruthy() }) diff --git a/src/api/http-error.ts b/src/api/http-error.ts index 6e6e637..2d183b4 100644 --- a/src/api/http-error.ts +++ b/src/api/http-error.ts @@ -2,9 +2,11 @@ export class HttpError implements Error { public name = 'HttpError' public message: string - // eslint-disable-next-line @typescript-eslint/camelcase - constructor(public response: GoogleAppsScript.URL_Fetch.HTTPResponse) { - this.message = `${response.getResponseCode()}: ${response.getContentText()}` + constructor( + public url: string, + public response: GoogleAppsScript.URL_Fetch.HTTPResponse // eslint-disable-line @typescript-eslint/camelcase + ) { + this.message = `${url} - ${response.getResponseCode()}: ${response.getContentText()}` } public isInvalidTestingRequest(): boolean { 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/v2/client.test.ts b/src/api/v2/client.test.ts index c6c3a2d..0d6f1d5 100644 --- a/src/api/v2/client.test.ts +++ b/src/api/v2/client.test.ts @@ -11,11 +11,11 @@ test('HttpError#isInvalidTestingRequest', () => { 200, '{"message":"Testing Apikey is only allowed to ticker ending with \\"01\\""}' )() - const error1 = new HttpError(res1) + const error1 = new HttpError('https://example.com', res1) expect(error1.isInvalidTestingRequest()).toBeTruthy() const res2 = useMockedUrlFetchApp(403, '{"message": "Forbidden"}')() - const error2 = new HttpError(res2) + const error2 = new HttpError('https://example.com', res2) expect(error2.isInvalidTestingRequest()).toBeFalsy() }) diff --git a/src/api/v2/client.ts b/src/api/v2/client.ts index cfcc112..461da63 100644 --- a/src/api/v2/client.ts +++ b/src/api/v2/client.ts @@ -16,7 +16,7 @@ export class BuffettCodeApiClientV2 { const code = res.getResponseCode() const content = res.getContentText() - const error = new HttpError(res) + const error = new HttpError(url, res) if ( Math.floor(code / 100) === 4 || Math.floor(code / 100) === 5 || @@ -29,8 +29,8 @@ export class BuffettCodeApiClientV2 { try { json = JSON.parse(content) } catch (e) { - console.error('JSON parsing error', code, content) - throw new HttpError(res) + console.error('JSON parsing error', url, code, content) + throw new HttpError(url, res) } return json 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 b30c498..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' @@ -18,11 +20,11 @@ describe('BuffettCodeApiClientV3', () => { 200, '{"message":"Testing apikey is not allowed"}' )() - const error1 = new HttpError(res1) + const error1 = new HttpError('https://example.com', res1) expect(error1.isInvalidTestingRequest()).toBeTruthy() const res2 = useMockedUrlFetchApp(403, '{"message": "Forbidden"}')() - const error2 = new HttpError(res2) + const error2 = new HttpError('https://example.com', res2) expect(error2.isInvalidTestingRequest()).toBeFalsy() }) @@ -85,7 +87,7 @@ describe('BuffettCodeApiClientV3', () => { const client = new BuffettCodeApiClientV3('foo') const ticker = '2371' - expect(client.company(ticker)).toEqual(company[ticker]) + expect(client.company(ticker)).toEqual(company['data']) expect(mockFetch.mock.calls.length).toBe(1) expect(mockFetch.mock.calls[0].length).toBe(2) expect(mockFetch.mock.calls[0][0]).toBe( @@ -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/api/v3/client.ts b/src/api/v3/client.ts index 0e028cd..3f10b68 100644 --- a/src/api/v3/client.ts +++ b/src/api/v3/client.ts @@ -21,7 +21,7 @@ export class BuffettCodeApiClientV3 { const code = res.getResponseCode() const content = res.getContentText() - const error = new HttpError(res) + const error = new HttpError(url, res) if ( Math.floor(code / 100) === 4 || Math.floor(code / 100) === 5 || @@ -34,8 +34,8 @@ export class BuffettCodeApiClientV3 { try { json = JSON.parse(content) } catch (e) { - console.error('JSON parsing error', code, content) - throw new HttpError(res) + console.error('JSON parsing error', url, code, content) + throw new HttpError(url, res) } return json @@ -56,7 +56,7 @@ export class BuffettCodeApiClientV3 { const options = this.defaultOptions() const res = BuffettCodeApiClientV3.request(url, options) - return res[ticker] + return res['data'] } public quarter(ticker: string, period: YearQuarterParam): Quarter { 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-result.test.ts b/src/custom-functions/v3/bcode-result.test.ts index 08bb2ac..1a62991 100644 --- a/src/custom-functions/v3/bcode-result.test.ts +++ b/src/custom-functions/v3/bcode-result.test.ts @@ -50,3 +50,21 @@ test('format (null)', () => { expect(new BcodeResult('net_sales', null, '円').format(true, false)).toBe('') expect(new BcodeResult('net_sales', null, '円').format(true, true)).toBe('') }) + +test('format (object)', () => { + const segmentMember = { + segments: [], + periods: [], + values: [] + } + const result = new BcodeResult('segment_member', segmentMember, 'なし') + + expect(result.format(false, false)).toEqual(JSON.stringify(segmentMember)) + expect(result.format(true, false)).toEqual(JSON.stringify(segmentMember)) + expect(result.format(false, true)).toEqual( + JSON.stringify(segmentMember) + 'なし' + ) + expect(result.format(true, true)).toEqual( + JSON.stringify(segmentMember) + 'なし' + ) +}) diff --git a/src/custom-functions/v3/bcode-result.ts b/src/custom-functions/v3/bcode-result.ts index a11a892..a958cf7 100644 --- a/src/custom-functions/v3/bcode-result.ts +++ b/src/custom-functions/v3/bcode-result.ts @@ -4,7 +4,7 @@ import { Formatter } from '~/services/formatter' export class BcodeResult { constructor( public name: string, - public value: number | string | null, + public value: number | string | object | null, public unit: string ) {} @@ -29,6 +29,10 @@ export class BcodeResult { } } + if (typeof value === 'object') { + value = JSON.stringify(value) + } + if (isWithUnits) { if (typeof value == 'number') { return Formatter.commaStyled(value) + unit 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/entities/v3/daily.test.ts b/src/entities/v3/daily.test.ts new file mode 100644 index 0000000..1f9d1c3 --- /dev/null +++ b/src/entities/v3/daily.test.ts @@ -0,0 +1,24 @@ +import * as response from '~/__mocks__/fixtures/v3/daily' +import { Daily } from '~/entities/v3/daily' + +const daily = Daily.fromResponse(response) + +test('period', () => { + expect(daily.period()).toEqual(new Date('2020-09-06')) +}) + +test('propertyNames', () => { + expect(daily.propertyNames()).toEqual( + Object.keys(response['column_description']) + ) +}) + +test('labelOf', () => { + expect(daily.labelOf('day')).toEqual('日付') + expect(daily.labelOf('market_capital')).toEqual('時価総額') +}) + +test('unitOf', () => { + expect(daily.unitOf('day')).toEqual('単位無し') + expect(daily.unitOf('market_capital')).toEqual('円') +}) diff --git a/src/entities/v3/daily.ts b/src/entities/v3/daily.ts index 0f71817..cef71d6 100644 --- a/src/entities/v3/daily.ts +++ b/src/entities/v3/daily.ts @@ -1,8 +1,40 @@ -export class Daily { +import { HasColumnDescription, HasPeriod } from '~/entities/v3/interface' + +export class Daily implements HasColumnDescription, HasPeriod { constructor(readonly data: object, readonly columnDescription: object) { // noop } + period(): Date { + return new Date(this.data['day']) + } + + propertyNames(): string[] { + return Object.keys(this.columnDescription) + } + + labelOf(propertyName: string): string | null { + const desc = this.columnDescriptionOf(propertyName) + if (desc) { + return desc['name_jp'] + } else { + return null + } + } + + unitOf(propertyName: string): string | null { + const desc = this.columnDescriptionOf(propertyName) + if (desc) { + return desc['unit'] + } else { + return null + } + } + + private columnDescriptionOf(propertyName: string): string | null { + return this.columnDescription[propertyName] + } + static fromResponse(response: object): Daily { return new Daily(response['data'], response['column_description']) } diff --git a/src/entities/v3/interface.ts b/src/entities/v3/interface.ts new file mode 100644 index 0000000..a339a69 --- /dev/null +++ b/src/entities/v3/interface.ts @@ -0,0 +1,11 @@ +export interface HasColumnDescription { + propertyNames(): string[] + + labelOf(propertyName: string): string | null + + unitOf(propertyName: string): string | null +} + +export interface HasPeriod { + period(): T +} diff --git a/src/entities/v3/quarter.test.ts b/src/entities/v3/quarter.test.ts new file mode 100644 index 0000000..a806aea --- /dev/null +++ b/src/entities/v3/quarter.test.ts @@ -0,0 +1,25 @@ +import * as response from '~/__mocks__/fixtures/v3/quarter' +import { Quarter } from '~/entities/v3/quarter' +import { YearQuarter } from '~/fiscal-periods/year-quarter' + +const quarter = Quarter.fromResponse(response) + +test('period', () => { + expect(quarter.period()).toEqual(new YearQuarter(2018, 1)) +}) + +test('propertyNames', () => { + expect(quarter.propertyNames()).toEqual( + Object.keys(response['column_description']) + ) +}) + +test('labelOf', () => { + expect(quarter.labelOf('fiscal_year')).toEqual('会計年度') + expect(quarter.labelOf('net_sales')).toEqual('売上') +}) + +test('unitOf', () => { + expect(quarter.unitOf('fiscal_year')).toEqual('なし') + expect(quarter.unitOf('net_sales')).toEqual('円') +}) diff --git a/src/entities/v3/quarter.ts b/src/entities/v3/quarter.ts index 51f90f5..f98031e 100644 --- a/src/entities/v3/quarter.ts +++ b/src/entities/v3/quarter.ts @@ -1,8 +1,44 @@ -export class Quarter { +import { HasColumnDescription, HasPeriod } from '~/entities/v3/interface' +import { YearQuarter } from '~/fiscal-periods/year-quarter' + +export class Quarter implements HasColumnDescription, HasPeriod { constructor(readonly data: object, readonly columnDescription: object) { // noop } + period(): YearQuarter { + return new YearQuarter( + this.data['fiscal_year'], + this.data['fiscal_quarter'] + ) + } + + propertyNames(): string[] { + return Object.keys(this.columnDescription) + } + + labelOf(propertyName: string): string | null { + const desc = this.columnDescriptionOf(propertyName) + if (desc) { + return desc['name_jp'] + } else { + return null + } + } + + unitOf(propertyName: string): string | null { + const desc = this.columnDescriptionOf(propertyName) + if (desc) { + return desc['unit'] + } else { + return null + } + } + + private columnDescriptionOf(propertyName: string): string | null { + return this.columnDescription[propertyName] + } + static fromResponse(response: object): Quarter { return new Quarter(response['data'], response['column_description']) } 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..dcce539 --- /dev/null +++ b/src/fiscal-periods/lq-with-offset.test.ts @@ -0,0 +1,35 @@ +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('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) + 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) +}) + +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 new file mode 100644 index 0000000..fd1c612 --- /dev/null +++ b/src/fiscal-periods/lq-with-offset.ts @@ -0,0 +1,34 @@ +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( + `LQ offset must be negative but ${offset} given.` + ) + } + } + + 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(this.pattern) + if (matches == undefined) { + throw new ParseError(`Invalid LQ format: ${str}`) + } + + 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 new file mode 100644 index 0000000..b9b4727 --- /dev/null +++ b/src/fiscal-periods/ly-with-offset.test.ts @@ -0,0 +1,35 @@ +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('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) + 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) +}) + +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 new file mode 100644 index 0000000..d3979b4 --- /dev/null +++ b/src/fiscal-periods/ly-with-offset.ts @@ -0,0 +1,34 @@ +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( + `LY offset must be negative but ${offset} given.` + ) + } + } + + 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(this.pattern) + if (matches == undefined) { + throw new ParseError(`Invalid LY format: ${str}`) + } + + 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 fbda466..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('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('2020LQ')).toThrow(ParseError) - expect(() => PeriodParser.parse('LYQ3')).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 d6e5ea2..a1268ec 100644 --- a/src/fiscal-periods/year-quarter-param.test.ts +++ b/src/fiscal-periods/year-quarter-param.test.ts @@ -1,49 +1,56 @@ import { - InvalidLYLQError, InvalidYearError, 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) - expect(() => new YearQuarterParam('LY', 3)).toThrow(InvalidLYLQError) expect(() => new YearQuarterParam(2018, 0)).toThrow(InvalidQuarterError) 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(InvalidLYLQError) - expect(() => new YearQuarterParam('LY', 1)).toThrow(InvalidLYLQError) - expect(() => new YearQuarterParam('LY', 4)).toThrow(InvalidLYLQError) - expect(() => new YearQuarterParam('LY', 5)).toThrow(InvalidLYLQError) - 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(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', 'LQ').isLatestYear()).toBeTruthy() + expect(new YearQuarterParam(LY, 3).isLatestYear()).toBeTruthy() expect(new YearQuarterParam(2020, 3).isLatestYear()).toBeFalsy() }) test('isLatestQuarter', () => { - expect(new YearQuarterParam('LY', 'LQ').isLatestYear()).toBeTruthy() - expect(new YearQuarterParam(2020, 3).isLatestYear()).toBeFalsy() + expect(new YearQuarterParam(2020, LQ).isLatestQuarter()).toBeTruthy() + expect(new YearQuarterParam(2020, 3).isLatestQuarter()).toBeFalsy() }) test('fromYearQuarter', () => { @@ -59,14 +66,13 @@ test('parse', () => { expect(YearQuarterParam.parse('2020q3')).toEqual( new YearQuarterParam(2020, 3) ) - expect(YearQuarterParam.parse('LYLQ')).toEqual( - new YearQuarterParam('LY', 'LQ') - ) - expect(YearQuarterParam.parse('lylq')).toEqual( - new YearQuarterParam('LY', 'LQ') + 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)) + expect(() => YearQuarterParam.parse('20Q3')).toThrow(ParseError) - expect(() => YearQuarterParam.parse('LYQ3')).toThrow(InvalidLYLQError) - expect(() => YearQuarterParam.parse('2020LQ')).toThrow(InvalidLYLQError) 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 d319380..f6f54a0 100644 --- a/src/fiscal-periods/year-quarter-param.ts +++ b/src/fiscal-periods/year-quarter-param.ts @@ -1,27 +1,23 @@ import { - InvalidLYLQError, InvalidYearError, 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() && this.isLatestQuarter()) || - (this.isLatestYear() && !this.isLatestQuarter()) - ) { - // NOTE: 現状ではLY/LQの同時指定しかサポートしない - throw new InvalidLYLQError() - } - - if (!this.isLatestYear() && year < 1) { - throw new InvalidYearError() + 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)) { - throw new InvalidQuarterError() + if (typeof quarter === 'number' && (quarter < 1 || quarter > 4)) { + throw new InvalidQuarterError(`Invalid quarter value: ${quarter}`) } } @@ -30,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) @@ -38,24 +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 } - // XXX: LYLQの同時指定や-1などの相対値指定をどうするか確認する 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) } diff --git a/src/fiscal-periods/year-quarter.ts b/src/fiscal-periods/year-quarter.ts index 48b8b1e..5333071 100644 --- a/src/fiscal-periods/year-quarter.ts +++ b/src/fiscal-periods/year-quarter.ts @@ -3,11 +3,11 @@ import { InvalidYearError, InvalidQuarterError } from '~/fiscal-periods/error' export class YearQuarter { constructor(public year: number, public quarter: number) { if (year < 1) { - throw new InvalidYearError() + throw new InvalidYearError(`Invalid year value: ${year}`) } if (quarter < 1 || quarter > 4) { - throw new InvalidQuarterError() + throw new InvalidQuarterError(`Invalid quarter value: ${quarter}`) } } diff --git a/src/services/csv-exporter.test.ts b/src/services/csv-exporter.test.ts index fa5a316..2f26731 100644 --- a/src/services/csv-exporter.test.ts +++ b/src/services/csv-exporter.test.ts @@ -1,15 +1,11 @@ -import { default as quarter } from '~/__mocks__/fixtures/v2/quarter-property' -import { QuarterProperty } from '~/api/v2/quarter-property' +import { default as quarter } from '~/__mocks__/fixtures/v3/quarter' import { YearQuarter } from '~/fiscal-periods/year-quarter' import { CsvExporter } from '~/services/csv-exporter' import { QuarterCache } from '~/services/quarter-cache' jest.mock('~/setting', () => jest.requireActual('~/__mocks__/setting')) -jest.mock('~/api/v2/client', () => - jest.requireActual('~/__mocks__/api/v2/client') -) -jest.mock('~/api/v2/quarter-property', () => - jest.requireActual('~/__mocks__/api/v2/quarter-property') +jest.mock('~/api/v3/client', () => + jest.requireActual('~/__mocks__/api/v3/client') ) jest.mock('~/services/quarter-property-cache', () => jest.requireActual('~/__mocks__/services/quarter-property-cache') @@ -21,8 +17,11 @@ jest.mock('~/services/quarter-cache', () => jest.requireActual('~/__mocks__/services/quarter-cache') ) -const fiscalYearIndex = Object.keys(quarter).indexOf('fiscal_year') -const fiscalQuarterIndex = Object.keys(quarter).indexOf('fiscal_quarter') +const columnDescription = quarter['column_description'] +const propertyNames = Object.keys(columnDescription) +const fiscalYearIndex = propertyNames.indexOf('fiscal_year') +const fiscalQuarterIndex = propertyNames.indexOf('fiscal_quarter') +const segmentMemberIndex = propertyNames.indexOf('segment_member') describe('generateData', () => { test('uncached', () => { @@ -34,7 +33,7 @@ describe('generateData', () => { const data = CsvExporter.generateData(ticker, from, to, today) - expect(data.length).toBe(QuarterProperty.names().length + 1) + expect(data.length).toBe(propertyNames.length + 1) expect(data[0].length).toBe(3 + 1) expect(data[0]).toEqual(['キー', '項目名', '単位', from]) expect(data[fiscalYearIndex + 1]).toEqual([ @@ -49,6 +48,12 @@ describe('generateData', () => { 'なし', 1.0 ]) + expect(data[segmentMemberIndex + 1]).toEqual([ + 'segment_member', + 'セグメント情報', + 'なし', + JSON.stringify(quarter['data']['segment_member']) + ]) expect( QuarterCache.getData(ticker, new YearQuarter(2018, 1))['net_sales'] ).toBe(12513000000.0) @@ -65,7 +70,7 @@ describe('generateData', () => { const data = CsvExporter.generateData(ticker, from, to, today) - expect(data.length).toBe(QuarterProperty.names().length + 1) + expect(data.length).toBe(propertyNames.length + 1) expect(data[0].length).toBe(3 + 1) expect(data[0]).toEqual(['キー', '項目名', '単位', from]) expect(data[fiscalYearIndex + 1]).toEqual([ @@ -80,6 +85,12 @@ describe('generateData', () => { 'なし', 1.0 ]) + expect(data[segmentMemberIndex + 1]).toEqual([ + 'segment_member', + 'セグメント情報', + 'なし', + JSON.stringify(quarter['data']['segment_member']) + ]) expect( QuarterCache.getData(ticker, new YearQuarter(2018, 1))['net_sales'] ).toBe(12513000000.0) diff --git a/src/services/csv-exporter.ts b/src/services/csv-exporter.ts index ec5b252..b5d34c0 100644 --- a/src/services/csv-exporter.ts +++ b/src/services/csv-exporter.ts @@ -1,7 +1,6 @@ import { CompanyService } from '~/api/company-service' import { OndemandApiPeriodRange } from '~/api/ondemand-api-period-range' -import { CachingBuffettCodeApiClientV2 } from '~/api/v2/caching-client' -import { CachingQuarterProperty } from '~/api/v2/caching-quarter-property' +import { CachingBuffettCodeApiClientV3 } from '~/api/v3/caching-client' import { YearQuarter } from '~/fiscal-periods/year-quarter' import { YearQuarterParam } from '~/fiscal-periods/year-quarter-param' import { YearQuarterRange } from '~/fiscal-periods/year-quarter-range' @@ -12,6 +11,16 @@ export class CsvExporter { // } + static format( + value: number | string | object | null + ): number | string | null { + if (typeof value == 'object') { + value = JSON.stringify(value) + } + + return value + } + static generateData( ticker, from: string, @@ -23,7 +32,7 @@ export class CsvExporter { const range = new YearQuarterRange(fromYearQuarter, toYearQuarter) const setting = Setting.load() - const client = new CachingBuffettCodeApiClientV2(setting.token) + const client = new CachingBuffettCodeApiClientV3(setting.token) const companyService = new CompanyService(ticker, client, today) if (!companyService.isSupportedTicker()) { throw new Error('<<サポートされていないtickerです>>') @@ -65,8 +74,8 @@ export class CsvExporter { } const sortedQuarters = quarters.slice().sort((a, b) => { - const labelA = `${a['fiscal_year']}Q${a['fiscal_quarter']}` - const labelB = `${b['fiscal_year']}Q${b['fiscal_quarter']}` + const labelA = a.period().toString() + const labelB = b.period().toString() if (labelA > labelB) { return 1 } else if (labelA < labelB) { @@ -76,18 +85,16 @@ export class CsvExporter { } }) - const quarterLabels = sortedQuarters.map( - quarter => `${quarter['fiscal_year']}Q${quarter['fiscal_quarter']}` + const quarter = quarters[0] + const quarterLabels = sortedQuarters.map(quarter => + quarter.period().toString() ) const header = [['キー', '項目名', '単位', ...quarterLabels]] - const values = CachingQuarterProperty.names().map(name => { - const data = sortedQuarters.map(quarter => quarter[name]) - return [ - name, - CachingQuarterProperty.labelOf(name), - CachingQuarterProperty.unitOf(name), - ...data - ] + const values = quarter.propertyNames().map(name => { + const data = sortedQuarters.map(quarter => + this.format(quarter.data[name]) + ) + return [name, quarter.labelOf(name), quarter.unitOf(name), ...data] }) return [...header, ...values] diff --git a/src/version.ts b/src/version.ts index 8e36562..dd0c1e1 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = 'v10' +export const version = 'v11'