个人在 leetcode 上也刷了不少题,最近打算用 typescript 来再次做以前用 javascript 完成的题目,看看近一年到底进步了没有。因此配置一个刷题的环境,支持单元测试和 typescript,并且能够帮我减少一些重复的类型定义,本文是记录项目创建的过程
配置项目
-
首先使用
yarn init创建项目 -
leetcode 支持 typescript,目前如果你想用 typescript 进行刷题的话还是比较麻烦的,可能你需要先 tsc 编译成 js 文件再本地检验结果是否正确,因此来集成 typescript
yarn add typscript --dev // 命令执行完就可以看到生成了 tsconfig.json npx tsc --init -
项目需要集成单元测试,以便判断题解的程序是否正确。我选用了 Jest 这个框架,注意需要在 tsconfig.json 的 types 数组下添加 "jest"
// 添加 jest yarn add jest @types/jest --dev -
在 node 环境下你只能使用 CJS 模块,因此需要通过 babel 来配置 ES 模块,顺便通过 bebel 来配置支持 typescript
yarn add --dev babel-jest @babel/core @babel/preset-env yarn add --dev @babel/preset-typescript然后在工程的根目录下创建一个 babel.config.js 文件用于配置与你当前Node版本兼容的Babel
module.exports = { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript', ], }; -
最后添加测试指令,在 package.json 添加下列代码
"scripts": { "test": "jest" },
工程目录结构
zsx-leet (project name)
├── src
│ ├── tests // tests 文件夹,存放测试代码,以 ***.spec.ts 命名
│ | ├── 1.spec.ts // 根据同名文件编写的测试代码
| | └── ...
│ ├── utils // utils 文件夹,存放工具函数
│ ├── 1.ts // 根据 leetcode 题目编号编写的解题代码
│ └── ...
导出常用类型
leetcode 有大量关于链表和二叉树等数据结构的题目,例如 function inorderTraversal(root: TreeNode | null): number[] {};, 我们总不能每一个相关的问题都定义一遍 TreeNode 吧。因此,最好全局导出,在项目根目录下建立 global.d.ts 文件,一处定义,全局使用
// global.d.ts
declare class ListNode {
public val: number;
public next: ListNode | null;
constructor(val?: number, next?: ListNode | null);
}
declare class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null);
}
转换函数
针对二叉树,leetcode 为了方便描述,抽象出的形式是一个包含了数字和 null 的数组,针对链表,则是一个纯数字数组,因此编写一个函数,将数组转换成正常的二叉树和链表,下面举例是链表的,二叉树比较复杂,具体地请看项目代码,都做好了注释当然。当然工具函数也做了单元测试,并且测试通过
// convert.ts
const toLinkedList = (list: number[]): ListNode | null => {
const len: number = list.length
if (len === 0) {
return null
}
const initListNode: ListNode = { val: -1, next: null }
const result: ListNode = { ...initListNode }
let temp: ListNode | null = result
for (let i = 0; i < list.length; i++) {
temp.val = list[i]
temp.next = (i !== len - 1) ? { ...initListNode } : null
temp = temp.next
}
return result
}
// convert.spec.ts
test("toLinkedList", () => {
expect(toLinkedList([])).toEqual(null)
expect(toLinkedList([3, 2, 6])).toEqual({ val: 3, next: { val: 2, next: { val: 6, next: null } } })
expect(toLinkedList([3, 1, 2, 4])).toEqual({ val: 3, next: { val: 1, next: { val: 2, next: { val: 4, next: null } } } })
})
Jest 使用
Jest 的具体使用可以去看文档,但我们只需要用到几个常用的 api 即可,toBe 和 toEqual 在对应基本类型时无区别,如果函数返回的是引用类型,那么就只能使用 toEqual 了,这意味着 toEqual 适用所有情况,not 表示不等于,当你需要验证实际结果与预期结果不一致时就可以使用。
// https://leetcode-cn.com/problems/trapping-rain-water/
import trap from "../42";
test("42", () => {
expect(trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1])).toBe(6)
expect(trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1])).toEqual(6)
expect(trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1])).not.toBe(5)
expect(trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1])).not.toEqual(5)
})
如果一个题目 [100000, -100000] 或 [-100000, 100000] 都为正确,那么该怎么使用呢?这时就需要使用 toContainEqual 了。toContain 则同理,不过只适用于基本类型。
// https://leetcode.cn/problems/restore-the-array-from-adjacent-pairs/
import restoreArray from '../1743';
test("1743", () => {
expect(restoreArray([[2, 1], [3, 4], [3, 2]])).toEqual([1, 2, 3, 4])
expect([[-3, 1, 4, -2], [-2, 4, 1, -3]]).toContainEqual(restoreArray([[4, -2], [1, 4], [-3, 1]]))
expect([[100000, -100000], [-100000, 100000]]).toContainEqual(restoreArray([[100000, -100000]]))
})
如果一个题目的答案是一个数组,数组内只要包含 1 2 3 4 就算正确,那么该怎么测试呢?还使用 toContainEqual 的话,就需要写 n * (n - 1) / 2 个可能答案了,数组长度少的话还可以,多了呢?因此,写一个函数来检测数组中所有的 item 是否一一对应
interface CheckItem {
value: string
match: boolean
}
/**
* 适用于答案为数组,但顺序不固定的题目
* @param items1
* @param items2
* @returns
*/
const detectItems = <T>(items1: T[], items2: T[]): boolean => {
if (items1.length !== items2.length) {
return false
}
const checkItems: CheckItem[] = items1.map(item => ({value: String(item), match: false}))
for (const item of items2) {
const idx: number = checkItems.findIndex(checkItem => checkItem.value === String(item) && checkItem.match === false)
if (idx === -1) {
return false
} else {
checkItems[idx].match = true
}
}
return true
}
export {
detectItems
}
使用也挺简单
https://leetcode.cn/problems/subsets-ii/
import subsetsWithDup from "../90";
import {detectItems} from "../utils/detect";
test("90", () => {
expect(
detectItems<number[]>(
subsetsWithDup([1, 2, 2]),
[[], [1], [1, 2], [1, 2, 2], [2], [2, 2]]
)
).toBeTruthy()
expect(
detectItems<number[]>(
subsetsWithDup([0]),
[[], [0]]
)
).toBeTruthy()
})
使用 yarn test 来测试代码是否正确,但通常我会使用 yarn test file_name,因为 yarn test 会测试 tests 文件夹下所有的代码是否正确,而我们通常只需要测试一小部分。对于测试用例的编写,一般只需要通过题目的示例,你就可以放心的提交了,如果不通过的话,分析原因,改正,把错误的用例添加进去
创建文件脚本
每次做一道题的时候,我们都需要创建两个文件,xxx.ts 和 xxx.spec.ts ,然后再在里面写导出导入的代码,再加上 test("xxx", () => {}) 之类的模板代码,因此写一个脚本来自动创建文件。
"use strict";
const fs = require('fs');
const params = process.argv.slice(2);
if (params.length < 2) {
throw new Error("You must enter enough parameters")
}
const [serial, code] = params;
const isFn = code.includes("function");
/** 可能为 function name 或 class name */
let exportName = "";
if (isFn) {
for (let i = 9; i < code.length; i++) {
if (code[i] === "(") {
break;
}
exportName += code[i];
}
} else {
for (let i = 6; i < code.length; i++) {
if (code[i] === " ") {
break;
}
exportName += code[i];
}
}
/** 用来判断使用 toBe 或 toEqual */
let type = '';
let toFn = '';
if (isFn) {
const startIdx = code.indexOf(')') + 3
for (let i = startIdx; i < code.length; i++) {
if (code[i] === ' ') {
break
}
type += code[i]
}
}
if (['number', 'string', 'boolean'].includes(type)) {
toFn = 'toBe'
} else {
toFn = 'toEqual'
}
/** 如果是 function ,替换成箭头函数 */
const mainTemplate = isFn ?
`${(code ?? "")
.replace("function", "const")
.replace("(", " = (")
.replace("{", "=> {")
.replace(";", "")}
export default ${exportName}
` :
`${code}
export default ${exportName}
`;
/** 测试文件模板 */
let expectTemplate = '';
if (isFn) {
expectTemplate = `expect(${exportName}()).${toFn}()
expect(${exportName}()).${toFn}()
expect(${exportName}()).${toFn}()`
}
const testTemplate = `import ${exportName} from '../${serial}';
test("${serial}", () => {
${expectTemplate}
})
`
const createFiles = () => {
// 生成基本文件
fs.writeFile(`./src/${serial}.ts`, mainTemplate, (error) => {
if (error) {
throw error
}
})
// 生成测试文件
fs.writeFile(`./src/tests/${serial}.spec.ts`, testTemplate, (error) => {
if (error) {
throw error
}
})
}
// 判断文件是否存在,并作对应的处理
fs.access(`./src/${serial}.ts`, (error) => {
if (error) /** 文件不存在 */ {
createFiles()
} else /** 文件存在 */ {
throw new Error("The file already exists")
}
})
使用也挺简单,node create xxx "function fn(){}" ,这样大大提高了效率。
尾声
文章发表在这里,希望得到大佬的指点,欢迎大家讨论!!!