From e553e7cd67fc1436a9735df9520df850370b044e Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 3 May 2026 13:15:44 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20numtheory-engine=20+=20GCD=20Euclidean?= =?UTF-8?q?=20lesson=20(l=E1=BB=9Bp=206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/lessons/uoc-chung-lon-nhat/copy.vi.js | 29 ++++++++ src/lib/numtheory-engine/gcd.js | 60 +++++++++++++++ src/lib/numtheory-engine/gcd.test.js | 73 +++++++++++++++++++ src/lib/numtheory-engine/index.js | 1 + 4 files changed, 163 insertions(+) create mode 100644 src/lib/lessons/uoc-chung-lon-nhat/copy.vi.js create mode 100644 src/lib/numtheory-engine/gcd.js create mode 100644 src/lib/numtheory-engine/gcd.test.js create mode 100644 src/lib/numtheory-engine/index.js diff --git a/src/lib/lessons/uoc-chung-lon-nhat/copy.vi.js b/src/lib/lessons/uoc-chung-lon-nhat/copy.vi.js new file mode 100644 index 0000000..0f72b83 --- /dev/null +++ b/src/lib/lessons/uoc-chung-lon-nhat/copy.vi.js @@ -0,0 +1,29 @@ +export const vi = { + slug: 'uoc-chung-lon-nhat', + topic: 'so-hoc', + grade: 'lop-6', + title: 'Ước chung lớn nhất (thuật toán Euclid)', + gradeLabel: 'Lớp 6', + intro: + 'Ước chung lớn nhất (ƯCLN) của hai số tự nhiên là số lớn nhất chia hết cả hai. Thuật toán Euclid tìm ƯCLN bằng cách thay liên tiếp (a, b) bằng (b, a mod b) cho đến khi b = 0 — khi đó a chính là ƯCLN.', + instruction: 'Nhập hai số (1 – 999) rồi nhấn "Bước" để xem từng bước rút gọn', + inputALabel: 'Số thứ nhất (a)', + inputBLabel: 'Số thứ hai (b)', + stepBtn: 'Bước', + autoBtn: 'Tự động', + resetBtn: 'Khởi động lại', + tableHeaderA: 'a', + tableHeaderB: 'b', + tableHeaderQ: 'q', + tableHeaderR: 'r', + rowDescription: 'a = q · b + r', + resultLabel: 'ƯCLN', + lcmLabel: 'BCNN', + theoremTitle: 'Thuật toán Euclid', + theoremStatement: + 'Với mọi a, b ≥ 0 (không cùng bằng 0): ƯCLN(a, b) = ƯCLN(b, a mod b). Khi b = 0, ƯCLN = a.', + exampleTitle: 'Ví dụ', + exampleBody: + 'Tìm ƯCLN(48, 18). Bước 1: 48 = 2·18 + 12. Bước 2: 18 = 1·12 + 6. Bước 3: 12 = 2·6 + 0. Vậy ƯCLN(48, 18) = 6, và BCNN(48, 18) = 48·18 / 6 = 144.', + nextTeaser: 'Sắp ra mắt: BCNN bằng phân tích thừa số nguyên tố', +}; diff --git a/src/lib/numtheory-engine/gcd.js b/src/lib/numtheory-engine/gcd.js new file mode 100644 index 0000000..b34b3de --- /dev/null +++ b/src/lib/numtheory-engine/gcd.js @@ -0,0 +1,60 @@ +/** + * @typedef {Readonly<{a: number, b: number, q: number, r: number}>} GcdStep + */ + +/** + * Greatest common divisor via Euclidean algorithm. + * Negative inputs are taken as absolute values; gcd(0, 0) returns 0. + * @param {number} a + * @param {number} b + * @returns {number} + */ +export function gcd(a, b) { + a = Math.abs(Math.trunc(a)); + b = Math.abs(Math.trunc(b)); + while (b !== 0) { + [a, b] = [b, a % b]; + } + return a; +} + +/** + * Least common multiple. Throws if both inputs are 0. + * @param {number} a + * @param {number} b + * @returns {number} + */ +export function lcm(a, b) { + a = Math.abs(Math.trunc(a)); + b = Math.abs(Math.trunc(b)); + if (a === 0 && b === 0) throw new Error('lcm(0, 0) is undefined'); + if (a === 0 || b === 0) return 0; + return (a * b) / gcd(a, b); +} + +/** + * Trace of the Euclidean reduction for visualization. + * Each row captures the pair before reduction plus quotient and remainder + * such that `a = q*b + r`. Last row's `b` is the gcd. + * For `b = 0` initial input, returns a single trivial row `{a, b:0, q:0, r:0}`. + * @param {number} a + * @param {number} b + * @returns {GcdStep[]} + */ +export function gcdSteps(a, b) { + let x = Math.abs(Math.trunc(a)); + let y = Math.abs(Math.trunc(b)); + /** @type {GcdStep[]} */ + const steps = []; + if (y === 0) { + return [{ a: x, b: 0, q: 0, r: 0 }]; + } + while (y !== 0) { + const q = Math.floor(x / y); + const r = x - q * y; + steps.push({ a: x, b: y, q, r }); + x = y; + y = r; + } + return steps; +} diff --git a/src/lib/numtheory-engine/gcd.test.js b/src/lib/numtheory-engine/gcd.test.js new file mode 100644 index 0000000..8dbd2ed --- /dev/null +++ b/src/lib/numtheory-engine/gcd.test.js @@ -0,0 +1,73 @@ +import { describe, it, expect } from 'vitest'; +import { gcd, lcm, gcdSteps } from './gcd.js'; + +describe('gcd', () => { + it('computes basic gcd', () => { + expect(gcd(48, 18)).toBe(6); + expect(gcd(18, 48)).toBe(6); // symmetric + expect(gcd(100, 75)).toBe(25); + }); + + it('handles coprime', () => { + expect(gcd(13, 17)).toBe(1); + expect(gcd(7, 5)).toBe(1); + }); + + it('handles zero', () => { + expect(gcd(0, 5)).toBe(5); + expect(gcd(7, 0)).toBe(7); + expect(gcd(0, 0)).toBe(0); + }); + + it('handles negatives via absolute value', () => { + expect(gcd(-48, 18)).toBe(6); + expect(gcd(48, -18)).toBe(6); + expect(gcd(-12, -8)).toBe(4); + }); + + it('truncates non-integer inputs', () => { + expect(gcd(48.7, 18.2)).toBe(6); + }); +}); + +describe('lcm', () => { + it('computes basic lcm', () => { + expect(lcm(4, 6)).toBe(12); + expect(lcm(48, 18)).toBe(144); + }); + + it('handles one zero as 0', () => { + expect(lcm(0, 5)).toBe(0); + expect(lcm(5, 0)).toBe(0); + }); + + it('throws on lcm(0, 0)', () => { + expect(() => lcm(0, 0)).toThrow(); + }); +}); + +describe('gcdSteps', () => { + it('produces canonical (48, 18) trace', () => { + const steps = gcdSteps(48, 18); + expect(steps).toEqual([ + { a: 48, b: 18, q: 2, r: 12 }, + { a: 18, b: 12, q: 1, r: 6 }, + { a: 12, b: 6, q: 2, r: 0 }, + ]); + }); + + it('last row b equals gcd', () => { + const steps = gcdSteps(100, 75); + expect(steps[steps.length - 1].b).toBe(gcd(100, 75)); + }); + + it('handles b=0 initial', () => { + expect(gcdSteps(7, 0)).toEqual([{ a: 7, b: 0, q: 0, r: 0 }]); + }); + + it('handles a < b (first row swaps via quotient=0)', () => { + const steps = gcdSteps(18, 48); + expect(steps[0]).toEqual({ a: 18, b: 48, q: 0, r: 18 }); + expect(steps[steps.length - 1].b).toBe(6); + }); +}); diff --git a/src/lib/numtheory-engine/index.js b/src/lib/numtheory-engine/index.js new file mode 100644 index 0000000..f2b8b34 --- /dev/null +++ b/src/lib/numtheory-engine/index.js @@ -0,0 +1 @@ +export { gcd, lcm, gcdSteps } from './gcd.js';