diff --git a/package.json b/package.json index 19d7364..c7cd414 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "build": "cpx 'appsscript.json' 'dist' -v && cpx 'src/ui/*.html' 'dist' -v && webpack", "watch": "webpack -w", "lint": "eslint 'src/**/*.ts'", - "test": "npm run lint && jest", + "jest": "jest", + "test": "npm run lint && npm run jest", "switch:local": "cp .clasp.local.json .clasp.json", "switch:dev": "cp .clasp.dev.json .clasp.json", "switch:prod": "cp .clasp.prod.json .clasp.json", diff --git a/src/__mocks__/api/v3/client.ts b/src/__mocks__/api/v3/client.ts index 521a6ee..f3dd955 100644 --- a/src/__mocks__/api/v3/client.ts +++ b/src/__mocks__/api/v3/client.ts @@ -3,6 +3,7 @@ import { default as company } from '~/__mocks__/fixtures/v3/company.js' import { default as daily } from '~/__mocks__/fixtures/v3/daily.js' import { default as quarter } from '~/__mocks__/fixtures/v3/quarter.js' import { HttpError } from '~/api/http-error' +import { Company } from '~/entities/v3/company' import { Daily } from '~/entities/v3/daily' import { Quarter } from '~/entities/v3/quarter' @@ -17,13 +18,13 @@ export class BuffettCodeApiClientV3 { this.mockQuarter.mockReturnValue(quarter) } - company(ticker: string): object { + company(ticker: string): Company { if (ticker !== '2371') { const res = new HTTPResnpose() throw new HttpError('/v3/company', res) } - return this.mockCompany()['data'] + return Company.fromResponse(this.mockCompany()) } quarter(ticker: string): Quarter { diff --git a/src/api/company-service.ts b/src/api/company-service.ts index 76e3a9e..8898859 100644 --- a/src/api/company-service.ts +++ b/src/api/company-service.ts @@ -1,4 +1,5 @@ import { BuffettCodeApiClientV3 } from '~/api/v3/client' +import { Company } from '~/entities/v3/company' import { DateParam } from '~/fiscal-periods/date-param' import { LqWithOffset } from '~/fiscal-periods/lq-with-offset' import { LyWithOffset } from '~/fiscal-periods/ly-with-offset' @@ -6,7 +7,7 @@ import { YearQuarter } from '~/fiscal-periods/year-quarter' import { YearQuarterParam } from '~/fiscal-periods/year-quarter-param' export class CompanyService { - public company: object + public company: Company constructor(public ticker: string, client: BuffettCodeApiClientV3, private today: Date = new Date()) { this.company = client.company(ticker) @@ -14,12 +15,12 @@ export class CompanyService { public convertYearQuarterParamToYearQuarter(period: YearQuarterParam): YearQuarter { if (period.year instanceof LyWithOffset) { - period.year = this.company['latest_fiscal_year'] + period.year.offset + period.year = this.company.data['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) + period.quarter = this.company.data['latest_fiscal_quarter'] + (period.quarter.offset % 4) if (period.quarter <= 0) { period.year -= 1 @@ -38,7 +39,7 @@ export class CompanyService { period = _period } - const fixedTierRange = this.company['fixed_tier_range'] + const fixedTierRange = this.company.data['fixed_tier_range'] const fixedTierOldestPeriod = new YearQuarter( fixedTierRange['oldest_fiscal_year'], fixedTierRange['oldest_fiscal_quarter'] @@ -51,7 +52,7 @@ export class CompanyService { return false } - const fixedTierRange = this.company['fixed_tier_range'] + const fixedTierRange = this.company.data['fixed_tier_range'] const fixedTierOldestDate = new Date(fixedTierRange['oldest_date']) return date.toDate().valueOf() < fixedTierOldestDate.valueOf() diff --git a/src/api/v3/caching-client.ts b/src/api/v3/caching-client.ts index 0d38fa4..a7d5c60 100644 --- a/src/api/v3/caching-client.ts +++ b/src/api/v3/caching-client.ts @@ -1,4 +1,5 @@ import { BuffettCodeApiClientV3 } from '~/api/v3/client' +import { Company } from '~/entities/v3/company' import { Daily } from '~/entities/v3/daily' import { Quarter } from '~/entities/v3/quarter' import { DateParam } from '~/fiscal-periods/date-param' @@ -12,7 +13,7 @@ export class CachingBuffettCodeApiClientV3 extends BuffettCodeApiClientV3 { super(token) } - company(ticker: string): object { + company(ticker: string): Company { const cached = CompanyCache.get(ticker) if (cached) { return cached diff --git a/src/api/v3/client.test.ts b/src/api/v3/client.test.ts index 20ab39f..9dc14b6 100644 --- a/src/api/v3/client.test.ts +++ b/src/api/v3/client.test.ts @@ -4,6 +4,7 @@ import * as quarter from '~/__mocks__/fixtures/v3/quarter' import { HttpError } from '~/api/http-error' import { useMockedUrlFetchApp } from '~/api/test-helper' import { BuffettCodeApiClientV3 } from '~/api/v3/client' +import { Company } from '~/entities/v3/company' import { Daily } from '~/entities/v3/daily' import { Quarter } from '~/entities/v3/quarter' import { DateParam } from '~/fiscal-periods/date-param' @@ -66,7 +67,7 @@ describe('BuffettCodeApiClientV3', () => { const client = new BuffettCodeApiClientV3('foo') const ticker = '2371' - expect(client.company(ticker)).toEqual(company['data']) + expect(client.company(ticker)).toEqual(Company.fromResponse(company)) 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/company?ticker=${ticker}`) diff --git a/src/api/v3/client.ts b/src/api/v3/client.ts index c4450c0..a2fcae3 100644 --- a/src/api/v3/client.ts +++ b/src/api/v3/client.ts @@ -1,5 +1,6 @@ import { HttpError } from '~/api/http-error' import { UrlBuilder } from '~/api/url-builder' +import { Company } from '~/entities/v3/company' import { Daily } from '~/entities/v3/daily' import { Quarter } from '~/entities/v3/quarter' import { DateParam } from '~/fiscal-periods/date-param' @@ -45,14 +46,14 @@ export class BuffettCodeApiClientV3 { } } - public company(ticker: string): object { + public company(ticker: string): Company { const endpoint = BuffettCodeApiClientV3.baseUrl + '/company' const builder = new UrlBuilder(endpoint, { ticker: ticker }) const url = builder.toString() const options = this.defaultOptions() const res = BuffettCodeApiClientV3.request(url, options) - return res['data'] + return Company.fromResponse(res) } public quarter(ticker: string, period: YearQuarterParam): Quarter { diff --git a/src/custom-functions/v3/bcode-company.test.ts b/src/custom-functions/v3/bcode-company.test.ts new file mode 100644 index 0000000..32e32fa --- /dev/null +++ b/src/custom-functions/v3/bcode-company.test.ts @@ -0,0 +1,16 @@ +import { CachingBuffettCodeApiClientV3 } from '~/api/v3/caching-client' +import { bcodeCompany } from '~/custom-functions/v3/bcode-company' +import { BcodeResult } from '~/custom-functions/v3/bcode-result' + +jest.mock('~/api/v3/client', () => jest.requireActual('~/__mocks__/api/v3/client')) +jest.mock('~/services/company-cache', () => jest.requireActual('~/__mocks__/services/company-cache')) + +test('bcodeCompany', () => { + const ticker = '2371' + const propertyName = 'company_name' + + const client = new CachingBuffettCodeApiClientV3('token') + const result = bcodeCompany(client, ticker, propertyName) + + expect(result).toEqual(new BcodeResult(propertyName, 'カカクコム', 'なし')) +}) diff --git a/src/custom-functions/v3/bcode-company.ts b/src/custom-functions/v3/bcode-company.ts new file mode 100644 index 0000000..585b272 --- /dev/null +++ b/src/custom-functions/v3/bcode-company.ts @@ -0,0 +1,17 @@ +import { BuffettCodeApiClientV3 } from '~/api/v3/client' +import { PropertyNotFoundError } from '~/custom-functions/error' +import { BcodeResult } from '~/custom-functions/v3/bcode-result' + +export function bcodeCompany(client: BuffettCodeApiClientV3, ticker: string, propertyName: string): BcodeResult { + const company = client.company(ticker) + + const property = company.columnDescription[propertyName] + if (property == undefined) { + throw new PropertyNotFoundError(`propetyName '${propertyName}' is not found.`) + } + + const value = company.data[propertyName] + const unit = company.unitOf(propertyName) + + return new BcodeResult(propertyName, value, unit) +} diff --git a/src/custom-functions/v3/bcode.ts b/src/custom-functions/v3/bcode.ts index 43ff088..41edc97 100644 --- a/src/custom-functions/v3/bcode.ts +++ b/src/custom-functions/v3/bcode.ts @@ -1,3 +1,4 @@ +import { bcodeCompany } from './bcode-company' import { CachingBuffettCodeApiClientV3 } from '~/api/v3/caching-client' import { bcodeDaily } from '~/custom-functions/v3/bcode-daily' import { bcodeQuarter } from '~/custom-functions/v3/bcode-quarter' @@ -8,7 +9,7 @@ import { Setting } from '~/setting' export function bcode( ticker: string, - period: string | Date, + intent: string | Date, propertyName: string, isRawValue = false, isWithUnits = false @@ -17,26 +18,36 @@ export function bcode( throw new Error('<>') } - if (!period) { - throw new Error('<>') + if (!intent) { + throw new Error('<>') } if (!propertyName) { throw new Error('<>') } - if (period instanceof Date) { - period = period.toISOString().substring(0, 10) - } - const setting = Setting.load() if (!setting.token) { throw new Error('<>') } + if (typeof intent === 'string' && intent.toUpperCase() === Setting.companyIntent) { + try { + const client = new CachingBuffettCodeApiClientV3(setting.token) + const result = bcodeCompany(client, ticker, propertyName) + return result.format(isRawValue, isWithUnits) + } catch (e) { + ErrorHandler.handle(e) + } + } + + if (intent instanceof Date) { + intent = intent.toISOString().substring(0, 10) + } + try { const client = new CachingBuffettCodeApiClientV3(setting.token) - const parsedPeriod = PeriodParser.parse(period) + const parsedPeriod = PeriodParser.parse(intent) let result: BcodeResult if (PeriodParser.isDateParam(parsedPeriod)) { result = bcodeDaily( diff --git a/src/entities/v3/company.test.ts b/src/entities/v3/company.test.ts new file mode 100644 index 0000000..9e5cc96 --- /dev/null +++ b/src/entities/v3/company.test.ts @@ -0,0 +1,18 @@ +import * as response from '~/__mocks__/fixtures/v3/company' +import { Company } from '~/entities/v3/company' + +const company = Company.fromResponse(response) + +test('propertyName', () => { + expect(company.propertyNames()).toEqual(Object.keys(response['column_description'])) +}) + +test('labelOf', () => { + expect(company.labelOf('company_name')).toEqual('会社名') + expect(company.labelOf('priority_market')).toEqual('優先市場') +}) + +test('unitOf', () => { + expect(company.unitOf('company_name')).toEqual('なし') + expect(company.unitOf('priority_market')).toEqual('なし') +}) diff --git a/src/entities/v3/company.ts b/src/entities/v3/company.ts new file mode 100644 index 0000000..d58671b --- /dev/null +++ b/src/entities/v3/company.ts @@ -0,0 +1,37 @@ +import { HasColumnDescription } from '~/entities/v3/interface' + +export class Company implements HasColumnDescription { + constructor(readonly data: object, readonly columnDescription: object) { + // noop + } + + propertyNames(): string[] { + return Object.keys(this.data) + } + + 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): Company { + return new Company(response['data'], response['column_description']) + } +} diff --git a/src/main.ts b/src/main.ts index bbc66f5..53f1e5e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -37,7 +37,7 @@ global.exportCsv = exportCsv * 指定した銘柄の財務数字や指標を取得します。 * * @param {"6501"} ticker 銘柄コード - * @param {"2017Q4"} period 会計期間 (例: 四半期 '2017Q4', 日付 '2020-09-06') + * @param {"2017Q4"} intent 会計期間または識別子 (例: 四半期 '2017Q4', 日付 '2020-09-06', 企業情報 'COMPANY') * @param {"net_sales"} propertyName 項目名 * @param {TRUE} isRawValue (オプション) 数値をRAWデータで表示するかどうか (デフォルト値: FALSE) * @param {TRUE} isWithUnits (オプション) 単位を末尾に付加するかどうか (デフォルト値: FALSE) @@ -45,6 +45,6 @@ global.exportCsv = exportCsv * @return 指定した銘柄の財務数字または指標 * @customfunction */ -global.BCODE = (ticker, period, propertyName, isRawValue = false, isWithUnits = false): number | string => { - return bcode(ticker, period, propertyName, isRawValue, isWithUnits) +global.BCODE = (ticker, intent, propertyName, isRawValue = false, isWithUnits = false): number | string => { + return bcode(ticker, intent, propertyName, isRawValue, isWithUnits) } diff --git a/src/services/company-cache.test.ts b/src/services/company-cache.test.ts index 32de284..f54928f 100644 --- a/src/services/company-cache.test.ts +++ b/src/services/company-cache.test.ts @@ -1,30 +1,47 @@ import * as companyFixture from '~/__mocks__/fixtures/v3/company' +import { Company } from '~/entities/v3/company' import { getMock, putMock } from '~/services/cache-test-helper' import { CompanyCache } from '~/services/company-cache' -const company = companyFixture['data'] +const company = Company.fromResponse(companyFixture) test('key', () => { expect(CompanyCache.key('2371')).toBe('company-2371') }) +test('columnDescriptionKey', () => { + expect(CompanyCache.columnDescriptionKey()).toBe('company-column-description') +}) + beforeEach(() => { jest.clearAllMocks() }) -test('get', () => { - getMock.mockReturnValueOnce(JSON.stringify(company)) - expect(CompanyCache.get('2371')).toEqual(company) - expect(CompanyCache.get('2371')).toBeNull() +describe('get', () => { + test('returns data if cache exists', () => { + getMock.mockReturnValueOnce(JSON.stringify(company.data)) + getMock.mockReturnValueOnce(JSON.stringify(company.columnDescription)) + expect(CompanyCache.get('2371')).toEqual(company) + + expect(getMock).toBeCalledTimes(2) + expect(getMock).nthCalledWith(1, 'company-2371') + expect(getMock).nthCalledWith(2, 'company-column-description') + }) + + test('returns null if cache does not exist', () => { + getMock.mockReturnValue(null) + expect(CompanyCache.get('2371')).toBeNull() - expect(getMock).toBeCalledTimes(2) - expect(getMock).nthCalledWith(1, 'company-2371') - expect(getMock).nthCalledWith(2, 'company-2371') + expect(getMock).toBeCalledTimes(2) + expect(getMock).nthCalledWith(1, 'company-2371') + expect(getMock).nthCalledWith(2, 'company-column-description') + }) }) test('put', () => { CompanyCache.put('2371', company) - expect(putMock).toBeCalledTimes(1) - expect(putMock).toBeCalledWith('company-2371', JSON.stringify(company), 21600) + expect(putMock).toBeCalledTimes(2) + expect(putMock).toBeCalledWith('company-2371', JSON.stringify(company.data), 21600) + expect(putMock).toBeCalledWith('company-column-description', JSON.stringify(company.columnDescription), 21600) }) diff --git a/src/services/company-cache.ts b/src/services/company-cache.ts index b0dcbaf..bd91958 100644 --- a/src/services/company-cache.ts +++ b/src/services/company-cache.ts @@ -1,3 +1,5 @@ +import { Company } from '~/entities/v3/company' + export class CompanyCache { static readonly prefix = 'company' @@ -9,9 +11,14 @@ export class CompanyCache { return `${this.prefix}-${ticker}` } - static get(ticker): object | null { + static columnDescriptionKey(): string { + return `${this.prefix}-column-description` + } + + private static getData(ticker: string): object | null { const cache = CacheService.getUserCache() - const cached = cache.get(this.key(ticker)) + const key = this.key(ticker) + const cached = cache.get(key) if (!cached) { return null } @@ -19,8 +26,39 @@ export class CompanyCache { return JSON.parse(cached) } - static put(ticker: string, company: object, expirationInSeconds = 21600): void { + private static getColumnDescription(): object | null { const cache = CacheService.getUserCache() - cache.put(this.key(ticker), JSON.stringify(company), expirationInSeconds) + const cached = cache.get(this.columnDescriptionKey()) + if (!cached) { + return null + } + + return JSON.parse(cached) + } + + static get(ticker: string): Company | null { + const cachedData = this.getData(ticker) + const cachedColumnDescription = this.getColumnDescription() + if (!cachedData || !cachedColumnDescription) { + return null + } + + return new Company(cachedData, cachedColumnDescription) + } + + private static putData(ticker: string, company: object, expirationInSeconds = 21600): void { + const cache = CacheService.getUserCache() + const key = this.key(ticker) + cache.put(key, JSON.stringify(company), expirationInSeconds) + } + + private static putColumnDescription(columnDescription: object, expirationInSeconds = 21600): void { + const cache = CacheService.getUserCache() + cache.put(this.columnDescriptionKey(), JSON.stringify(columnDescription), expirationInSeconds) + } + + static put(ticker: string, company: Company, expirationInSeconds = 21600): void { + this.putData(ticker, company.data, expirationInSeconds) + this.putColumnDescription(company.columnDescription, expirationInSeconds) } } diff --git a/src/setting.ts b/src/setting.ts index 91d45fb..a25dea6 100644 --- a/src/setting.ts +++ b/src/setting.ts @@ -12,6 +12,8 @@ export class Setting { static readonly defaultOndemandApiEnabled = false static readonly defaultOndemandApiCallMode = Setting.ondemandApiCallModes.DEFAULT + static readonly companyIntent = 'COMPANY' + private constructor( private _token: string, private _ondemandApiEnabled: boolean,