diff --git a/src/webparts/ctFormsPortal/forms/ct/components/utils/unitConversion.test.ts b/src/webparts/ctFormsPortal/forms/ct/components/utils/unitConversion.test.ts new file mode 100644 index 0000000..241e647 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/utils/unitConversion.test.ts @@ -0,0 +1,64 @@ +import { convertQuantity, normalizeUnit, unitsEquivalent } from './unitConversion'; + +describe('unitsEquivalent — синонимы одной единицы', () => { + // СТ №12: в заявке «Штука», у поставщика «шт.» — тумблер помечал «шт. *» (пересчёт невозможен). + it('«Штука» = «шт.» = «шт» = «ед.»', () => { + expect(unitsEquivalent('Штука', 'шт.')).toBe(true); + expect(unitsEquivalent('шт', 'ед.')).toBe(true); + expect(unitsEquivalent('Штук', 'ШТ')).toBe(true); + }); + + it('полные и краткие названия: килограмм, литр, тонна, метр', () => { + expect(unitsEquivalent('Килограмм', 'кг')).toBe(true); + expect(unitsEquivalent('Литр', 'л.')).toBe(true); + expect(unitsEquivalent('Тонна', 'т')).toBe(true); + expect(unitsEquivalent('Метр', 'м.')).toBe(true); + }); + + it('квадратные, кубические и погонные метры в разных написаниях', () => { + expect(unitsEquivalent('кв.м.', 'м²')).toBe(true); + expect(unitsEquivalent('куб.м.', 'м3')).toBe(true); + expect(unitsEquivalent('пог.м', 'м.п.')).toBe(true); + }); + + it('комплект, упаковка, пара — свои синонимы', () => { + expect(unitsEquivalent('Комплект', 'компл.')).toBe(true); + expect(unitsEquivalent('Упаковка', 'уп.')).toBe(true); + expect(unitsEquivalent('пара', 'пар')).toBe(true); + }); + + it('разные единицы не равны', () => { + expect(unitsEquivalent('шт', 'компл')).toBe(false); + expect(unitsEquivalent('кг', 'т')).toBe(false); + }); +}); + +describe('convertQuantity — пересчёт в ед. изм. заявки', () => { + it('тонны в килограммы', () => { + expect(convertQuantity(0.5, 'т', 'кг')).toBe(500); + expect(convertQuantity(1, 'Тонна', 'Килограмм')).toBe(1000); + }); + + it('миллиметры в метры, литры в кубометры', () => { + expect(convertQuantity(1500, 'мм', 'м')).toBeCloseTo(1.5); + expect(convertQuantity(1000, 'л', 'куб.м.')).toBeCloseTo(1); + }); + + // Штуки в комплекты без кратности не перевести — честно говорим «невозможно». + it('штучные единицы между собой и с массой не пересчитываются', () => { + expect(convertQuantity(1, 'шт', 'компл')).toBeNull(); + expect(convertQuantity(1, 'шт', 'кг')).toBeNull(); + }); + + it('незнакомая единица — пересчёт невозможен', () => { + expect(convertQuantity(1, 'бухта', 'м')).toBeNull(); + }); +}); + +describe('normalizeUnit', () => { + // Раньше точку на конце срезали ПОСЛЕ синонимов, и «кв.м.» не распознавалось. + it('точки и пробелы не мешают распознать единицу', () => { + expect(normalizeUnit('кв.м.')).toBe(normalizeUnit('м2')); + expect(normalizeUnit(' шт. ')).toBe(normalizeUnit('Штука')); + }); +}); diff --git a/src/webparts/ctFormsPortal/forms/ct/components/utils/unitConversion.ts b/src/webparts/ctFormsPortal/forms/ct/components/utils/unitConversion.ts index a1f9b2f..71e7201 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/utils/unitConversion.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/utils/unitConversion.ts @@ -3,61 +3,84 @@ // поставщика из ед. КП в ед. изм. позиции заявки. // Несовместимые размерности (напр. кг ↔ м³ — нужна плотность) не пересчитываются. +// Штучные единицы — каждая своя «размерность»: шт ↔ компл без кратности не перевести. +type Dimension = 'mass' | 'length' | 'area' | 'volume' | 'piece' | 'set' | 'pack' | 'pair' | 'roll' | 'sheet' | 'bag'; + interface UnitDef { - dim: 'mass' | 'length' | 'area' | 'volume'; + dim: Dimension; toBase: number; // множитель к базовой единице размерности } -// Базовые единицы: масса — кг, длина — м, площадь — м², объём — л. +// Канонические единицы. Базовые: масса — кг, длина — м, площадь — м², объём — л, штучные — сама единица. const UNIT_TABLE: Record = { - // масса (база — кг) 'мг': { dim: 'mass', toBase: 0.000001 }, 'г': { dim: 'mass', toBase: 0.001 }, - 'гр': { dim: 'mass', toBase: 0.001 }, - 'грамм': { dim: 'mass', toBase: 0.001 }, 'кг': { dim: 'mass', toBase: 1 }, - 'килограмм': { dim: 'mass', toBase: 1 }, 'ц': { dim: 'mass', toBase: 100 }, - 'центнер': { dim: 'mass', toBase: 100 }, 'т': { dim: 'mass', toBase: 1000 }, - 'тн': { dim: 'mass', toBase: 1000 }, - 'тонна': { dim: 'mass', toBase: 1000 }, - 'тонн': { dim: 'mass', toBase: 1000 }, - // длина (база — м) 'мм': { dim: 'length', toBase: 0.001 }, 'см': { dim: 'length', toBase: 0.01 }, 'дм': { dim: 'length', toBase: 0.1 }, 'м': { dim: 'length', toBase: 1 }, - 'метр': { dim: 'length', toBase: 1 }, - 'пм': { dim: 'length', toBase: 1 }, 'км': { dim: 'length', toBase: 1000 }, - // площадь (база — м²) 'мм2': { dim: 'area', toBase: 0.000001 }, 'см2': { dim: 'area', toBase: 0.0001 }, 'дм2': { dim: 'area', toBase: 0.01 }, 'м2': { dim: 'area', toBase: 1 }, - // объём (база — л) 'мл': { dim: 'volume', toBase: 0.001 }, - 'см3': { dim: 'volume', toBase: 0.001 }, 'л': { dim: 'volume', toBase: 1 }, - 'дм3': { dim: 'volume', toBase: 1 }, 'м3': { dim: 'volume', toBase: 1000 }, + 'шт': { dim: 'piece', toBase: 1 }, + 'компл': { dim: 'set', toBase: 1 }, + 'упак': { dim: 'pack', toBase: 1 }, + 'пара': { dim: 'pair', toBase: 1 }, + 'рулон': { dim: 'roll', toBase: 1 }, + 'лист': { dim: 'sheet', toBase: 1 }, + 'мешок': { dim: 'bag', toBase: 1 }, }; -// Приводим написание единицы к ключу таблицы: нижний регистр, без пробелов, -// ³/² → 3/2, частые синонимы (куб.м → м3, кв.м → м2, пог.м → пм), без хвостовых точек. -export function normalizeUnit(raw: string | undefined): string { - let u = (raw ?? '').toLowerCase().trim(); - u = u.replace(/³/g, '3').replace(/²/g, '2'); - u = u.replace(/\s+/g, ''); - u = u.replace(/^куб\.?м3?$/, 'м3').replace(/^кубм$/, 'м3'); - u = u.replace(/^кв\.?м2?$/, 'м2').replace(/^квм$/, 'м2'); - u = u.replace(/^пог\.?м$/, 'пм'); - u = u.replace(/\.+$/, ''); - return u; +// Синонимы → каноническая единица. Ключи — уже без пробелов, точек и дефисов, в нижнем регистре +// (см. cleanUnit): «Штука», «шт.», «кв.м.», «м.п.», «к-т» и т.п. В заявках и КП пишут по-разному — +// без словаря «Штука» и «шт.» считались разными единицами, и тумблер помечал их «пересчёт невозможен». +const UNIT_ALIASES: Record = { + 'миллиграмм': 'мг', + 'гр': 'г', 'грамм': 'г', 'граммов': 'г', + 'килограмм': 'кг', 'килограммов': 'кг', 'килограмма': 'кг', + 'центнер': 'ц', + 'тн': 'т', 'тонна': 'т', 'тонн': 'т', 'тонны': 'т', + 'миллиметр': 'мм', 'сантиметр': 'см', 'дециметр': 'дм', 'километр': 'км', + 'метр': 'м', 'метров': 'м', 'метра': 'м', + 'пм': 'м', 'мп': 'м', 'погм': 'м', 'метрпогонный': 'м', 'погонныйметр': 'м', + 'квм': 'м2', 'квм2': 'м2', 'квадратныйметр': 'м2', 'метрквадратный': 'м2', + 'кубм': 'м3', 'кубм3': 'м3', 'куб': 'м3', 'кубометр': 'м3', 'кубическийметр': 'м3', 'метркубический': 'м3', + 'см3': 'мл', 'дм3': 'л', + 'миллилитр': 'мл', 'литр': 'л', 'литров': 'л', 'литра': 'л', + 'штука': 'шт', 'штук': 'шт', 'штуки': 'шт', 'ед': 'шт', 'единица': 'шт', 'единиц': 'шт', 'pcs': 'шт', + 'комплект': 'компл', 'комплектов': 'компл', 'кмпл': 'компл', 'кт': 'компл', + 'упаковка': 'упак', 'упаковок': 'упак', 'уп': 'упак', + 'пар': 'пара', 'пары': 'пара', + 'рул': 'рулон', 'рулонов': 'рулон', + 'листов': 'лист', 'листа': 'лист', + 'меш': 'мешок', 'мешков': 'мешок', +}; + +// Нижний регистр, ³/² → 3/2, без пробелов, точек и дефисов: «кв. м.» → «квм», «к-т» → «кт». +function cleanUnit(raw: string | undefined): string { + return (raw ?? '') + .toLowerCase() + .replace(/³/g, '3') + .replace(/²/g, '2') + .replace(/[\s.-]+/g, ''); } -// true, если единицы по сути одинаковые (тн == т == Тонна). +// Приводим написание единицы к канонической (ключ UNIT_TABLE). Незнакомую возвращаем очищенной — +// так одинаково написанные незнакомые единицы всё равно считаются равными. +export function normalizeUnit(raw: string | undefined): string { + const cleaned = cleanUnit(raw); + return UNIT_ALIASES[cleaned] ?? cleaned; +} + +// true, если единицы по сути одинаковые (тн == т == Тонна, шт. == Штука). export function unitsEquivalent(a: string | undefined, b: string | undefined): boolean { return normalizeUnit(a) === normalizeUnit(b); }