UNPKG

cefr-analyzer

Version:

分析英文文本中各CEFR级别(A1-C2)单词的数量

346 lines (303 loc) 11.4 kB
import { formatAnalysisResult, calculateComplexityScore, getComplexityLevel, generateSimpleVisualization, } from '../src/utils'; import { ICEFRAnalysisResult } from '../src/types'; // 更新权重系数以匹配 utils.ts 中的实现 const factors = { a1: 1, a2: 2.0, b1: 3.5, b2: 5, c1: 7, c2: 9.5, }; describe('Utils', () => { // 创建一个模拟的分析结果 const mockResult: ICEFRAnalysisResult = { totalWords: 40, levelCounts: { a1: 30, a2: 25, b1: 20, b2: 15, c1: 5, c2: 0, }, levelPercentages: { a1: 30, a2: 25, b1: 20, b2: 15, c1: 5, c2: 0, }, unknownWords: 5, unknownWordsList: ['unknown1', 'unknown2', 'unknown3', 'unknown4', 'unknown5'], wordsAtLevel: { a1: [ { word: 'word1', pos: 'NN', lemma: 'word1' }, { word: 'word2', pos: 'NN', lemma: 'word2' }, { word: 'word3', pos: 'NN', lemma: 'word3' }, { word: 'word4', pos: 'NN', lemma: 'word4' }, { word: 'word5', pos: 'NN', lemma: 'word5' }, { word: 'word6', pos: 'NN', lemma: 'word6' }, { word: 'word7', pos: 'NN', lemma: 'word7' }, { word: 'word8', pos: 'NN', lemma: 'word8' }, { word: 'word9', pos: 'NN', lemma: 'word9' }, { word: 'word10', pos: 'NN', lemma: 'word10' }, ], a2: [ { word: 'word11', pos: 'NN', lemma: 'word11' }, { word: 'word12', pos: 'NN', lemma: 'word12' }, { word: 'word13', pos: 'NN', lemma: 'word13' }, { word: 'word14', pos: 'NN', lemma: 'word14' }, { word: 'word15', pos: 'NN', lemma: 'word15' }, ], b1: [ { word: 'word16', pos: 'NN', lemma: 'word16' }, { word: 'word17', pos: 'NN', lemma: 'word17' }, { word: 'word18', pos: 'NN', lemma: 'word18' }, { word: 'word19', pos: 'NN', lemma: 'word19' }, { word: 'word20', pos: 'NN', lemma: 'word20' }, ], b2: [ { word: 'word21', pos: 'NN', lemma: 'word21' }, { word: 'word22', pos: 'NN', lemma: 'word22' }, { word: 'word23', pos: 'NN', lemma: 'word23' }, { word: 'word24', pos: 'NN', lemma: 'word24' }, { word: 'word25', pos: 'NN', lemma: 'word25' }, ], c1: [ { word: 'word26', pos: 'NN', lemma: 'word26' }, { word: 'word27', pos: 'NN', lemma: 'word27' }, { word: 'word28', pos: 'NN', lemma: 'word28' }, { word: 'word29', pos: 'NN', lemma: 'word29' }, { word: 'word30', pos: 'NN', lemma: 'word30' }, ], c2: [ { word: 'word31', pos: 'NN', lemma: 'word31' }, { word: 'word32', pos: 'NN', lemma: 'word32' }, { word: 'word33', pos: 'NN', lemma: 'word33' }, { word: 'word34', pos: 'NN', lemma: 'word34' }, { word: 'word35', pos: 'NN', lemma: 'word35' }, ], }, }; describe('formatAnalysisResult', () => { test('should format analysis result correctly', () => { const formatted = formatAnalysisResult(mockResult); // 检查格式化结果是否包含预期的内容 expect(formatted).toContain('总单词数: 40'); expect(formatted).toContain('已识别单词: 35'); expect(formatted).toContain('未识别单词: 5'); // 检查表格标题 expect(formatted).toContain('| 级别 | 单词数 | 百分比 |'); // 检查各级别数据 expect(formatted).toContain('| A1 | 30 | 30.00% |'); expect(formatted).toContain('| A2 | 25 | 25.00% |'); expect(formatted).toContain('| B1 | 20 | 20.00% |'); expect(formatted).toContain('| B2 | 15 | 15.00% |'); expect(formatted).toContain('| C1 | 5 | 5.00% |'); expect(formatted).toContain('| C2 | 0 | 0.00% |'); }); }); describe('calculateComplexityScore', () => { test('should calculate complexity score correctly for standard text (40 words)', () => { const result = calculateComplexityScore(mockResult); // 基础得分计算:(30*1 + 25*2 + 20*3.5 + 15*5 + 5*7 + 0*9.5) / 100 = 2.75 // 40词需要考虑长文本奖励:Math.min(1.0, Math.log(Math.max(1, 40 - 50)) / 10) = 0 const baseScore = (30 * factors.a1 + 25 * factors.a2 + 20 * factors.b1 + 15 * factors.b2 + 5 * factors.c1 + 0 * factors.c2) / 100; // 没有短文本惩罚和长文本奖励 expect(result.score).toBeCloseTo(baseScore, 2); expect(result.level).toBe(getComplexityLevel(baseScore)); expect(result.note).toBeUndefined(); }); test('should handle very short text (less than 10 words) with special note', () => { const veryShortResult: ICEFRAnalysisResult = { ...mockResult, totalWords: 8, levelPercentages: { a1: 40, a2: 30, b1: 20, b2: 10, c1: 0, c2: 0, }, }; const result = calculateComplexityScore(veryShortResult); // 基础得分计算:(40*1 + 30*2 + 20*3.5 + 10*5 + 0*7 + 0*9.5) / 100 = 2.2 const baseScore = (40 * factors.a1 + 30 * factors.a2 + 20 * factors.b1 + 10 * factors.b2 + 0 * factors.c1 + 0 * factors.c2) / 100; const totalWords = veryShortResult.totalWords; const shortPenalty = totalWords < 30 ? ((30 - totalWords) / 30) * 0.5 : 0; expect(result.score).toBeCloseTo(baseScore - shortPenalty, 2); expect(result.level).toBe(getComplexityLevel(baseScore - shortPenalty)); expect(result.note).toBe('Too short to evaluate CEFR level reliably.'); }); test('should apply short text penalty (less than 30 words)', () => { const shortResult: ICEFRAnalysisResult = { ...mockResult, totalWords: 25, levelPercentages: { a1: 30, a2: 25, b1: 20, b2: 15, c1: 10, c2: 0, }, }; const result = calculateComplexityScore(shortResult); // 基础得分:(30*1 + 25*2 + 20*3.5 + 15*5 + 10*7 + 0*9.5) / 100 = 3.05 const baseScore = (30 * factors.a1 + 25 * factors.a2 + 20 * factors.b1 + 15 * factors.b2 + 10 * factors.c1 + 0 * factors.c2) / 100; // 短文本惩罚:((30 - 25) / 30) * 0.5 = 0.083 const shortPenalty = ((30 - 25) / 30) * 0.5; // 调整后得分:3.05 - 0.083 = 2.967 const adjustedScore = baseScore - shortPenalty; expect(result.score).toBeCloseTo(adjustedScore, 2); expect(result.level).toBe(getComplexityLevel(adjustedScore)); expect(result.note).toBeUndefined(); }); test('should apply long text bonus (more than 50 words)', () => { const longResult: ICEFRAnalysisResult = { ...mockResult, totalWords: 500, levelPercentages: { a1: 20, a2: 20, b1: 20, b2: 20, c1: 15, c2: 5, }, }; const result = calculateComplexityScore(longResult); // 基础得分:(20*1 + 20*2 + 20*3.5 + 20*5 + 15*7 + 5*9.5) / 100 = 3.775 const baseScore = (20 * factors.a1 + 20 * factors.a2 + 20 * factors.b1 + 20 * factors.b2 + 15 * factors.c1 + 5 * factors.c2) / 100; // 长文本奖励:Math.min(1.0, Math.log(Math.max(1, 500 - 50)) / 10) 0.6 const longBonus = Math.min(1.0, Math.log(Math.max(1, 500 - 50)) / 10); // 调整后得分:3.775 + 0.6 = 4.375 const adjustedScore = baseScore + longBonus; expect(result.score).toBeCloseTo(adjustedScore, 2); expect(result.level).toBe(getComplexityLevel(adjustedScore)); expect(result.note).toBeUndefined(); }); test('should return score with two decimal places', () => { const result = calculateComplexityScore(mockResult); // 检查得分是否保留两位小数 expect(result.score.toString()).toMatch(/^\d+\.\d{1,2}$/); }); test('should return 0 for empty text', () => { const emptyResult: ICEFRAnalysisResult = { totalWords: 0, levelCounts: { a1: 0, a2: 0, b1: 0, b2: 0, c1: 0, c2: 0 }, levelPercentages: { a1: 0, a2: 0, b1: 0, b2: 0, c1: 0, c2: 0 }, unknownWords: 0, unknownWordsList: [], wordsAtLevel: { a1: [], a2: [], b1: [], b2: [], c1: [], c2: [] }, }; const result = calculateComplexityScore(emptyResult); expect(result.score).toBe(0); expect(result.level).toBe('a1'); }); test('should return 0 when no words are recognized', () => { const noRecognizedWordsResult: ICEFRAnalysisResult = { totalWords: 10, levelCounts: { a1: 0, a2: 0, b1: 0, b2: 0, c1: 0, c2: 0 }, levelPercentages: { a1: 0, a2: 0, b1: 0, b2: 0, c1: 0, c2: 0 }, unknownWords: 10, unknownWordsList: [ 'word1', 'word2', 'word3', 'word4', 'word5', 'word6', 'word7', 'word8', 'word9', 'word10', ], wordsAtLevel: { a1: [], a2: [], b1: [], b2: [], c1: [], c2: [] }, }; const result = calculateComplexityScore(noRecognizedWordsResult); expect(result.score).toBe(0); expect(result.level).toBe('a1'); }); }); describe('getComplexityLevel', () => { test('should return correct CEFR level for scores', () => { expect(getComplexityLevel(1.1)).toBe('a1'); expect(getComplexityLevel(1.2)).toBe('a2'); expect(getComplexityLevel(1.6)).toBe('a2'); expect(getComplexityLevel(1.7)).toBe('b1'); expect(getComplexityLevel(2.1)).toBe('b1'); expect(getComplexityLevel(2.2)).toBe('b2'); expect(getComplexityLevel(2.7)).toBe('b2'); expect(getComplexityLevel(2.8)).toBe('c1'); expect(getComplexityLevel(3.4)).toBe('c1'); expect(getComplexityLevel(3.5)).toBe('c2'); expect(getComplexityLevel(4.0)).toBe('c2'); }); }); describe('generateSimpleVisualization', () => { test('should generate ASCII visualization', () => { const visualization = generateSimpleVisualization(mockResult); /* ### CEFR级别分布可视化· A1: ███████████████ 30.00% A2: █████████████ 25.00% B1: ██████████ 20.00% B2: ████████ 15.00% C1: ███ 5.00% C2: 0.00% */ // 检查可视化结果是否包含预期的内容 expect(visualization).toContain('### CEFR级别分布可视化'); // 检查各级别的可视化条形图 // A1: 30% -> 15个字符 expect(visualization).toContain('A1: ███████████████ 30.00%'); // A2: 25% -> 12个字符 expect(visualization).toContain('A2: ████████████ 25.00%'); // B1: 20% -> 10个字符 expect(visualization).toContain('B1: ██████████ 20.00%'); // B2: 15% -> 7个字符 expect(visualization).toContain('B2: ███████ 15.00%'); // C1: 5% -> 2个字符 expect(visualization).toContain('C1: ██ 5.00%'); // C2: 0% -> 0个字符 expect(visualization).toContain('C2: 0.00%'); }); }); });