From a83e67a877a30a339607d36cf27ff515616969c9 Mon Sep 17 00:00:00 2001 From: SIY1121 Date: Mon, 4 Apr 2022 08:08:35 +0000 Subject: [PATCH] =?UTF-8?q?add:=20=E7=A7=91=E7=9B=AE=E7=95=AA=E5=8F=B7?= =?UTF-8?q?=E5=90=A6=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/usecase/searchCourse.test.ts | 494 ++++++++++++++----------- src/usecase/searchCourse.ts | 13 +- 2 files changed, 287 insertions(+), 220 deletions(-) diff --git a/__tests__/usecase/searchCourse.test.ts b/__tests__/usecase/searchCourse.test.ts index fe0cf44..be926f5 100644 --- a/__tests__/usecase/searchCourse.test.ts +++ b/__tests__/usecase/searchCourse.test.ts @@ -100,80 +100,136 @@ beforeAll(async () => { ) }) -test('キーワード検索単体(科目番号指定なし)', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: ['情報'], - codes: [], - searchMode: SearchMode.Cover, - offset: 0, - limit: 30, +describe('キーワードのみ', () => { + test('1つ指定', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: ['情報'], + codes: [], + searchMode: SearchMode.Cover, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => expect(c.name.includes('情報')).toBe(true)) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => expect(c.name.includes('情報')).toBe(true)) -}) -test('キーワード複数(科目番号指定なし)', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: ['情報', '人工知能'], - codes: [], - searchMode: SearchMode.Cover, - offset: 0, - limit: 30, + test('複数指定', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: ['情報', '人工知能'], + codes: [], + searchMode: SearchMode.Cover, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => + expect(c.name.includes('情報') && c.name.includes('人工知能')).toBe(true) + ) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => - expect(c.name.includes('情報') && c.name.includes('人工知能')).toBe(true) - ) }) -test('科目番号指定(キーワード指定なし)', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: [], - codes: ['FF'], - searchMode: SearchMode.Cover, - offset: 0, - limit: 30, +describe('科目番号のみ', () => { + test('一つ指定', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: ['FF'], + searchMode: SearchMode.Cover, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => expect(c.code.startsWith('FF')).toBe(true)) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => expect(c.code.startsWith('FF')).toBe(true)) -}) -test('科目番号指定複数(キーワード指定なし)', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: [], - codes: ['FF', 'GB'], - searchMode: SearchMode.Cover, - offset: 0, - limit: 30, + test('複数指定', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: ['FF', 'GB'], + searchMode: SearchMode.Cover, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => + expect(c.code.startsWith('FF') || c.code.startsWith('GB')).toBe(true) + ) + }) + + test('否定1つ指定', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: ['-0'], + searchMode: SearchMode.Cover, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => expect(!c.code.startsWith('0')).toBe(true)) + }) + + test('否定2つ指定', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: ['-GA', '-GB'], + searchMode: SearchMode.Cover, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => + expect(!c.code.startsWith('GA') && !c.code.startsWith('GB')).toBe(true) + ) + }) + + test('肯定否定複合', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: ['G', 'F', '-GB', '-FA'], + searchMode: SearchMode.Cover, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => + expect( + !c.code.startsWith('GB') && + !c.code.startsWith('FA') && + (c.code.startsWith('G') || c.code.startsWith('F')) + ).toBe(true) + ) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => - expect(c.code.startsWith('FF') || c.code.startsWith('GB')).toBe(true) - ) }) -test('キーワード指定&科目番号指定', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: ['科学'], - codes: ['FF'], - searchMode: SearchMode.Cover, - offset: 0, - limit: 30, +describe('キーワード&科目番号複合', () => { + test('キーワード指定&科目番号指定', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: ['科学'], + codes: ['FF'], + searchMode: SearchMode.Cover, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => + expect(c.name.includes('科学') && c.code.startsWith('FF')).toBe(true) + ) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => - expect(c.name.includes('科学') && c.code.startsWith('FF')).toBe(true) - ) }) test('キーワードなしで全部', async () => { @@ -242,178 +298,180 @@ test('キーワード指定&科目番号指定&時間割', async () => { ) }) -test('時間割 contain1', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: [], - codes: [], - searchMode: SearchMode.Contain, - timetable: { - SpringA: { - Tue: [false, true, true, false, false, false, false, false, false], - }, - SpringB: { - Tue: [false, true, true, false, false, false, false, false, false], +describe('時間割指定', () => { + test('時間割 contain1', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: [], + searchMode: SearchMode.Contain, + timetable: { + SpringA: { + Tue: [false, true, true, false, false, false, false, false, false], + }, + SpringB: { + Tue: [false, true, true, false, false, false, false, false, false], + }, }, - }, - offset: 0, - limit: 30, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => + checkScheduleContain(c, [ + { module: Module.SpringA, day: Day.Tue, periods: [1, 2] }, + { module: Module.SpringB, day: Day.Tue, periods: [1, 2] }, + ]) + ) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => - checkScheduleContain(c, [ - { module: Module.SpringA, day: Day.Tue, periods: [1, 2] }, - { module: Module.SpringB, day: Day.Tue, periods: [1, 2] }, - ]) - ) -}) -test('時間割 contain2', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: [], - codes: [], - searchMode: SearchMode.Contain, - timetable: { - SpringA: { - Tue: [false, false, true, false, false, false, false, false, false], - }, - SpringB: { - Tue: [false, false, true, false, false, false, false, false, false], + test('時間割 contain2', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: [], + searchMode: SearchMode.Contain, + timetable: { + SpringA: { + Tue: [false, false, true, false, false, false, false, false, false], + }, + SpringB: { + Tue: [false, false, true, false, false, false, false, false, false], + }, }, - }, - offset: 0, - limit: 30, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => + checkScheduleContain(c, [ + { module: Module.SpringA, day: Day.Tue, periods: [2] }, + { module: Module.SpringB, day: Day.Tue, periods: [2] }, + ]) + ) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => - checkScheduleContain(c, [ - { module: Module.SpringA, day: Day.Tue, periods: [2] }, - { module: Module.SpringB, day: Day.Tue, periods: [2] }, - ]) - ) -}) -test('時間割 contain3', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: [], - codes: [], - searchMode: SearchMode.Contain, - timetable: { - SpringA: fillAllDayWith(new Array(7).fill(true)), - SpringB: fillAllDayWith(new Array(7).fill(true)), - }, - offset: 0, - limit: 30, + test('時間割 contain3', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: [], + searchMode: SearchMode.Contain, + timetable: { + SpringA: fillAllDayWith(new Array(7).fill(true)), + SpringB: fillAllDayWith(new Array(7).fill(true)), + }, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => + checkScheduleContain(c, [ + { module: Module.SpringA }, + { module: Module.SpringB }, + ]) + ) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => - checkScheduleContain(c, [ - { module: Module.SpringA }, - { module: Module.SpringB }, - ]) - ) -}) -test('時間割 contain4', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: [], - codes: [], - searchMode: SearchMode.Contain, - timetable: fillAllModuleWith({ - Intensive: [true, false, false, false, false, false, false], - }), - offset: 0, - limit: 30, + test('時間割 contain4', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: [], + searchMode: SearchMode.Contain, + timetable: fillAllModuleWith({ + Intensive: [true, false, false, false, false, false, false], + }), + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => + checkScheduleContain(c, [{ day: Day.Intensive, periods: [0] }]) + ) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => - checkScheduleContain(c, [{ day: Day.Intensive, periods: [0] }]) - ) -}) -test('時間割 cover1', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: [], - codes: [], - searchMode: SearchMode.Cover, - timetable: { - SpringA: { - Wed: [false, false, false, false, false, true, true], + test('時間割 cover1', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: [], + searchMode: SearchMode.Cover, + timetable: { + SpringA: { + Wed: [false, false, false, false, false, true, true], + }, }, - }, - offset: 0, - limit: 30, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => + checkScheduleCover(c, [ + { module: Module.SpringA, day: Day.Wed, periods: [5, 6] }, + ]) + ) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => - checkScheduleCover(c, [ - { module: Module.SpringA, day: Day.Wed, periods: [5, 6] }, - ]) - ) -}) -test('時間割 cover2', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: [], - codes: [], - searchMode: SearchMode.Cover, - timetable: { - SpringA: { - Wed: [false, false, false, false, false, false, true], + test('時間割 cover2', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: [], + searchMode: SearchMode.Cover, + timetable: { + SpringA: { + Wed: [false, false, false, false, false, false, true], + }, }, - }, - offset: 0, - limit: 30, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => + checkScheduleCover(c, [ + { module: Module.SpringA, day: Day.Wed, periods: [6] }, + ]) + ) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => - checkScheduleCover(c, [ - { module: Module.SpringA, day: Day.Wed, periods: [6] }, - ]) - ) -}) -test('時間割 cover3', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: [], - codes: [], - searchMode: SearchMode.Cover, - timetable: { - SpringA: fillAllDayWith(new Array(7).fill(true)), - }, - offset: 0, - limit: 30, + test('時間割 cover3', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: [], + searchMode: SearchMode.Cover, + timetable: { + SpringA: fillAllDayWith(new Array(7).fill(true)), + }, + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => checkScheduleCover(c, [{ module: Module.SpringA }])) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => checkScheduleCover(c, [{ module: Module.SpringA }])) -}) -test('時間割 cover4', async () => { - const res = await searchCourseUseCase({ - year: 2020, - keywords: [], - codes: [], - searchMode: SearchMode.Cover, - timetable: fillAllModuleWith({ - Intensive: new Array(8).fill(true), - }), - offset: 0, - limit: 30, + test('時間割 cover4', async () => { + const res = await searchCourseUseCase({ + year: 2020, + keywords: [], + codes: [], + searchMode: SearchMode.Cover, + timetable: fillAllModuleWith({ + Intensive: new Array(8).fill(true), + }), + offset: 0, + limit: 30, + }) + expect(res.length > 0).toBe(true) + expect(res.length <= 30).toBe(true) + res.forEach((c) => checkScheduleCover(c, [{ day: Day.Intensive }])) }) - expect(res.length > 0).toBe(true) - expect(res.length <= 30).toBe(true) - res.forEach((c) => checkScheduleCover(c, [{ day: Day.Intensive }])) }) diff --git a/src/usecase/searchCourse.ts b/src/usecase/searchCourse.ts index 3fb1306..cb965b1 100644 --- a/src/usecase/searchCourse.ts +++ b/src/usecase/searchCourse.ts @@ -22,8 +22,17 @@ const searchNameRegexp = (names: string[]) => /** * 指定された科目番号に前方一致でヒットする正規表現を生成 */ -const searchCodeRegexp = (codes: string[]) => - `^(${codes.map(escapeRegex).join('|')})` +const searchCodeRegexp = (codes: string[]) => { + const positive = codes.filter((c) => !c.startsWith('-')).map(escapeRegex) + const negative = codes + .filter((c) => c.startsWith('-')) + .map((c) => c.substring(1)) + .map(escapeRegex) + let exp = '^' + if (positive.length > 0) exp += `(?=${positive.join('|')})` + if (negative.length > 0) exp += `(?!${negative.join('|')})` + return exp +} /** * キーワードと科目番号で検索する条件sqlを生成