From d7715639440dd6f53fd30a8eae23aa9708beaca9 Mon Sep 17 00:00:00 2001 From: Noriaki Uchiyama Date: Wed, 3 Jun 2020 20:50:20 +0900 Subject: [PATCH] [WIP] implement timetable hooks --- __tests__/hooks/useTimetable.test.ts | 79 ++++++++++++++++++++++++++ src/hooks/useTimetable.ts | 84 ++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 __tests__/hooks/useTimetable.test.ts create mode 100644 src/hooks/useTimetable.ts diff --git a/__tests__/hooks/useTimetable.test.ts b/__tests__/hooks/useTimetable.test.ts new file mode 100644 index 00000000..50e0088f --- /dev/null +++ b/__tests__/hooks/useTimetable.test.ts @@ -0,0 +1,79 @@ +import { renderHook, act } from '@testing-library/react-hooks'; + +import useTimetable, { TimetableProps } from '~/hooks/useTimetable'; + +describe('useTimetable hooks', () => { + it('set initial timetable', () => { + const timetable = { + schedule: [0, 1, 2, 3, 4, 5, 6], + // 06:00, 06:30, 07:10, 22:50, 23:30, 00:30 + data: [7200, 9000, 11400, 67800, 70200, 73800], + }; + const currentTime = new Date('2020-05-28T07:09:30').getTime(); + const { result } = renderHook(() => useTimetable(timetable, currentTime)); + expect(result.current.index).toBe(0); + expect(result.current.remaining).toBe(30); + }); + + describe('時間経過で保持する時刻表データも進む', () => { + let timetable: TimetableProps; + beforeEach(() => { + timetable = { + schedule: [0, 1, 2, 3, 4, 5, 6], + // 06:00, 06:30, 07:10, 22:50, 23:30, 00:30 + data: [7200, 9000, 11400, 67800, 70200, 73800], + }; + }); + + it('発車時刻ちょうどの場合は残り時間は`0`', () => { + const currentTime = new Date('2020-05-28T07:10:00').getTime(); + const { result } = renderHook(() => useTimetable(timetable, currentTime)); + expect(result.current.remaining).toBe(0); + }); + + it('発車済みの時刻は削除される', () => { + const currentTime = new Date('2020-05-28T06:29:59').getTime(); + const elapsedTime = currentTime + 2000; // 2 seconds + + const { result } = renderHook(() => useTimetable(timetable, currentTime)); + expect(result.current.remaining).toBe(1); + + act(() => { + result.current.tick(elapsedTime); + }); + expect(result.current.remaining).toBe(2399); + }); + + it('発車時刻を越えたとき`index`が先頭`0`であれば維持', () => { + const currentTime = new Date('2020-05-28T06:29:59').getTime(); + const elapsedTime = currentTime + 2000; // 2 seconds + + const { result } = renderHook(() => useTimetable(timetable, currentTime)); + expect(result.current.index).toBe(0); + + act(() => { + result.current.tick(elapsedTime); + }); + expect(result.current.index).toBe(0); + }); + + it('発車時刻を越えたとき`index`が先頭`1`以上であれば`-1`される', () => { + const currentTime = new Date('2020-05-28T06:29:59').getTime(); + const elapsedTime = currentTime + 2000; // 2 seconds + + const { result } = renderHook(() => useTimetable(timetable, currentTime)); + expect(result.current.index).toBe(0); + + act(() => { + result.current.next(); + result.current.next(); + }); + expect(result.current.index).toBe(2); + + act(() => { + result.current.tick(elapsedTime); + }); + expect(result.current.index).toBe(1); + }); + }); +}); diff --git a/src/hooks/useTimetable.ts b/src/hooks/useTimetable.ts new file mode 100644 index 00000000..a2ec8765 --- /dev/null +++ b/src/hooks/useTimetable.ts @@ -0,0 +1,84 @@ +import { useState, useCallback, useRef } from 'react'; + +export type TimetableProps = { + schedule: number[]; + data: number[]; +}; + +export type TimetableHook = { + index: number; + value: number; + tick: (nextTime: number) => void; + next: () => void; + remaining: number; +}; + +const getSecondsSince4am: (time: number) => number = (time) => { + const d = new Date(time); + const hours = (d.getHours() - 4) * 60 * 60; + const minutes = d.getMinutes() * 60; + const seconds = d.getSeconds(); + return hours + minutes + seconds; +}; + +const getSlicedData: (data: number[], time: number) => number[] = ( + data, + time, +) => { + const currentTimeAsSeconds = getSecondsSince4am(time); + return data.filter((s) => s >= currentTimeAsSeconds); +}; + +const useTimetable = ( + timetable: TimetableProps, + currentTime: number = Date.now(), +): TimetableHook => { + const time = useRef(currentTime); + const [currentIndex, setIndex] = useState(0); + const [currentData, setData] = useState( + getSlicedData(timetable.data, time.current), + ); + + const value = currentData[currentIndex]; + const remaining = value - getSecondsSince4am(time.current); + + const next = useCallback(() => { + setIndex((prevIndex) => { + if (prevIndex >= currentData.length - 1) { + return currentData.length - 1; + } + return prevIndex + 1; + }); + }, [currentData.length]); + + const prev = useCallback(() => { + setIndex((prevIndex) => { + if (prevIndex <= 0) { + return 0; + } + return prevIndex - 1; + }); + }, []); + + const tick = useCallback( + (nextTime) => { + time.current = nextTime; + const nextData = getSlicedData(timetable.data, time.current); + if (currentData.length !== nextData.length) { + prev(); + } + setData(nextData); + }, + [timetable.data, currentData.length, prev], + ); + + return { + index: currentIndex, + value, + tick, + next, + remaining, + }; +}; + +export default useTimetable;