@reekon-tools/boldr-utils 1.0.13 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2025] [REEKON Tools]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # BOLDR Utils
2
+
3
+ Utilies for BOLDR Pro
4
+
5
+ ## Intro
6
+
7
+ BOLDR Utils is a collection of types and utility functions written to be used across BOLDR web and mobile for consistency and reliability. This package must:
8
+
9
+ - Be consumable in React Native (Metro bundler)
10
+ - Be consumable in Remix (Node/ESM, often using Vite or Webpack)
11
+ - Be written in TypeScript
12
+ - Be published cleanly to npm
package/package.json CHANGED
@@ -1,23 +1,26 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.0.13",
4
- "main": "src/index.ts",
5
- "types": "src/index.ts",
6
- "exports": {
7
- ".": {
8
- "import": "./src/index.ts",
9
- "types": "./src/index.ts"
10
- }
11
- },
12
- "files": ["src"],
3
+ "version": "1.1.0",
4
+ "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
+ "author": "REEKON Tools",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
13
  "scripts": {
14
14
  "build": "tsc",
15
- "test": "jest"
15
+ "test": "vitest",
16
+ "coverage": "vitest run --coverage",
17
+ "format": "prettier --write .",
18
+ "check-format": "prettier --check .",
19
+ "check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
20
+ "ci": "yarn run check-format && vitest run && yarn run build && yarn run check-exports"
16
21
  },
17
22
  "keywords": [],
18
- "author": "",
19
- "license": "ISC",
20
- "repository": "https://gitlab.com/reekon-tools/software/rock-pro/boldr-types#",
23
+ "repository": "https://gitlab.com/reekon-tools/software/rock-pro/boldr-utils#",
21
24
  "dependencies": {
22
25
  "mathjs": "^11.11.0"
23
26
  },
@@ -25,13 +28,17 @@
25
28
  "react": "^18.0.0"
26
29
  },
27
30
  "devDependencies": {
28
- "@types/jest": "^29.5.14",
31
+ "@arethetypeswrong/cli": "^0.18.1",
29
32
  "@types/node": "^22.15.18",
30
33
  "@types/react": "^19.0.8",
31
- "jest": "^29.7.0",
32
- "ts-jest": "^29.3.3",
34
+ "@vitest/coverage-v8": "3.1.3",
35
+ "prettier": "^3.5.3",
33
36
  "ts-node": "^10.9.2",
34
- "typescript": "^5.8.3"
37
+ "typescript": "^5.8.3",
38
+ "vitest": "^3.1.3"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
35
42
  },
36
43
  "packageManager": "yarn@1.22.22"
37
44
  }
@@ -1,47 +0,0 @@
1
- import { FractionalTolerance, DecimalTolerance, Units } from '..';
2
- import { ColumnType } from '..';
3
- import { convertMicrometers } from '../utils/micrometersToUnit';
4
-
5
- export function createFormulaScope({
6
- group,
7
- mappings,
8
- columns,
9
- measurementMap,
10
- unit, // user preferred unit, e.g., 'in' or 'cm'
11
- }: {
12
- group: any;
13
- mappings: Record<string, string>;
14
- columns: any[];
15
- measurementMap: Map<string, any[]>;
16
- unit: string;
17
- }): Record<string, number> {
18
- const scope: Record<string, number> = {};
19
-
20
- for (const [variable, columnId] of Object.entries(mappings)) {
21
- const rawValue = group.columns.get(columnId);
22
- const columnDef = columns.find((c) => c.id === columnId);
23
-
24
- let parsedValue: number | undefined;
25
-
26
- if (columnDef?.type === ColumnType.Measurement) {
27
- const groupMeasurements = measurementMap.get(group.id) || [];
28
- const measurement = groupMeasurements.find((m) => m.id === rawValue);
29
- if (measurement?.value !== undefined) {
30
- // Here, the unit
31
- parsedValue = parseFloat(convertMicrometers(
32
- measurement.value,
33
- unit as Units,
34
- FractionalTolerance.Sixteenth,
35
- DecimalTolerance.Hundredth
36
- ).value);
37
- }
38
- } else {
39
- parsedValue =
40
- typeof rawValue === 'number' ? rawValue : parseFloat(rawValue || '');
41
- }
42
-
43
- scope[variable] = isNaN(parsedValue!) ? 0 : parsedValue!;
44
- }
45
-
46
- return scope;
47
- }
@@ -1,70 +0,0 @@
1
- import { ColumnConfig, ColumnType, Group, Units } from '..';
2
- import { createFormulaScope } from './createFormulaScope';
3
- import { compile, unit as mathUnit } from 'mathjs';
4
- import { normalizeUnitForMathJS } from './mathUnitMap';
5
-
6
- export function evaluateColumnValue({
7
- group,
8
- col,
9
- formulas,
10
- columns,
11
- measurementMap,
12
- userUnit,
13
- }: {
14
- group: Group;
15
- col: ColumnConfig;
16
- formulas: any[];
17
- columns: any[];
18
- measurementMap: Map<string, any[]>;
19
- userUnit: Units;
20
- }): any {
21
- // Normalize immediately
22
- const normalizedUnit = normalizeUnitForMathJS(userUnit);
23
-
24
- if (col.type === ColumnType.Measurement) {
25
- const value = group.columns.get(col.id);
26
- const groupMeasurements = measurementMap.get(group.id) || [];
27
- const measurement = groupMeasurements.find((m) => m.id === value);
28
- return measurement ?? value;
29
- }
30
-
31
- if (col.type === ColumnType.Formula) {
32
- const formula = formulas.find((f) => f.id === col.formulaId);
33
- if (!formula) {
34
- console.warn(`Missing formula with id ${col.formulaId}`);
35
- return null;
36
- }
37
-
38
- if (col.mappings === undefined) {
39
- return null;
40
- }
41
-
42
- try {
43
- const scope = createFormulaScope({
44
- group,
45
- mappings: col.mappings,
46
- columns,
47
- measurementMap,
48
- unit: normalizedUnit, // ✅ normalized!
49
- });
50
-
51
- console.log('Evaluating:', formula.expression, scope);
52
-
53
- const compiled = compile(formula.expression);
54
- const result = compiled.evaluate(scope);
55
-
56
- const asUnit = mathUnit(result, normalizedUnit);
57
-
58
- if (!asUnit.equalBase(mathUnit('1 um'))) {
59
- throw new Error(`Incompatible unit: ${asUnit.formatUnits()} is not compatible with ${normalizedUnit}`);
60
- }
61
-
62
- return Math.round(asUnit.toNumber('um'));
63
- } catch (error) {
64
- console.error(`Failed to evaluate formula: ${col.name}`, error);
65
- return null;
66
- }
67
- }
68
-
69
- return group.columns.get(col.id) ?? '';
70
- }
@@ -1,26 +0,0 @@
1
- import { Units } from "..";
2
-
3
- // Must convert our custom unit types
4
- export function normalizeUnitForMathJS(unit: Units | string): string {
5
- switch (unit) {
6
- case Units.Centimeters:
7
- case 'cm':
8
- return 'cm';
9
- case Units.Millimeters:
10
- case 'mm':
11
- return 'mm';
12
- case Units.Meters:
13
- case 'm':
14
- return 'm';
15
- case Units.Inches:
16
- case Units.FractionalInches:
17
- return 'in';
18
- case Units.Feet:
19
- case Units.FeetInchesDecimal:
20
- case Units.FeetInchesFractional:
21
- return 'ft';
22
- default:
23
- console.warn(`Unknown or unsupported unit: ${unit}, falling back to 'mm'`);
24
- return 'mm'; // fallback
25
- }
26
- }
@@ -1,18 +0,0 @@
1
-
2
- import { useState } from 'react';
3
- import { parseMeasurement } from '../utils/parseMeasurement';
4
-
5
- export const useParseMeasurement = () => {
6
- const [error, setError] = useState<string | null>(null);
7
-
8
- const parseMeasurementInput = (input: string, defaultUnit: string = 'mm') => {
9
- setError(null);
10
- const result = parseMeasurement(input, defaultUnit);
11
- if (result === null) {
12
- setError('Invalid measurement. Please provide a valid number and unit.');
13
- }
14
- return result;
15
- };
16
-
17
- return { parseMeasurementInput, error };
18
- };
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from './types/firestore';
2
- export { evaluateColumnValue } from './formulas/evaluateColumnValue';
@@ -1,202 +0,0 @@
1
- interface FirestoreDoc {
2
- id: string;
3
- }
4
-
5
- interface Timestamps {
6
- createdAt: Date;
7
- }
8
-
9
- interface CreatedBy {
10
- firstName: string;
11
- lastName: string;
12
- userId: string;
13
- }
14
-
15
- export interface Organization extends FirestoreDoc, Timestamps {
16
- name: string;
17
- createdBy: string;
18
- };
19
-
20
- export enum OrganizationRole {
21
- Admin = 'admin',
22
- Member = 'member',
23
- }
24
-
25
- export enum InvitationStatus {
26
- Pending = 'pending',
27
- Accepted = 'accepted',
28
- }
29
-
30
- export interface Invitation extends FirestoreDoc, Timestamps {
31
- delivery?: unknown;
32
- invitationCode: string;
33
- inviteeEmail: string;
34
- inviteeUid: string;
35
- inviterId: string;
36
- message: {
37
- subject: string;
38
- text: string;
39
- };
40
- organizationId: string;
41
- role: OrganizationRole;
42
- status: InvitationStatus
43
- };
44
-
45
- export interface OrganizationMember extends FirestoreDoc {
46
- email: string;
47
- firstName: string;
48
- lastName: string;
49
- joinedAt: Date;
50
- role: OrganizationRole;
51
- userId: string;
52
- };
53
-
54
- export interface Project extends FirestoreDoc, Timestamps {
55
- name: string;
56
- description: string;
57
- imageUrl: string | null;
58
- streetAddress: string;
59
- city: string;
60
- zipCode: string;
61
- state: string;
62
- }
63
-
64
- export enum FolderType {
65
- Project = 'project',
66
- Job = 'job',
67
- }
68
-
69
- export interface Folder extends FirestoreDoc {
70
- name: string;
71
- index: number;
72
- parentFolderId?: string | null;
73
- projectId: string | null;
74
- type: FolderType;
75
- }
76
-
77
- export interface Job extends FirestoreDoc, Timestamps {
78
- name: string;
79
- folderId: string;
80
- progress: number;
81
- projectId: string;
82
- totalTasks: string;
83
- }
84
-
85
- export interface Section {
86
- id: string;
87
- name: string;
88
- tableConfig: ColumnConfig[];
89
- measurements: string[];
90
- projectId: string;
91
- jobId: string;
92
- }
93
-
94
- export interface Group extends FirestoreDoc, Timestamps {
95
- projectId: string;
96
- jobId: string;
97
- sectionId: string;
98
- name: string;
99
- sortIndex: number;
100
- columns: Record<string, any>;
101
- }
102
-
103
- export enum ColumnType {
104
- Text = 'text',
105
- Number = 'number',
106
- Measurement = 'measurement',
107
- Boolean = 'boolean',
108
- Formula = 'formula',
109
- }
110
-
111
- export interface Formula extends FirestoreDoc, Timestamps {
112
- expression: string;
113
- name: string;
114
- varaibles: string[];
115
- }
116
-
117
- export interface ColumnConfig {
118
- id: string;
119
- name: string;
120
- type: ColumnType;
121
- formulaId?: string;
122
- mappings?: Record<string, string> // Used for formula mapping
123
- }
124
-
125
- export interface Row {
126
- [key: string]: any; // Dynamic keys representing column IDs, with their respective data
127
- }
128
-
129
- export interface Measurement extends FirestoreDoc, Timestamps, CreatedBy {
130
- projectId:string;
131
- jobId:string;
132
- groupId:string;
133
- sectionId:string;
134
- value: number;
135
- index: number;
136
- label: string;
137
- measurementIndex: number;
138
- unit: Units;
139
- }
140
-
141
- export enum Units {
142
- Centimeters = 'cm',
143
- Millimeters = 'mm',
144
- Meters = 'm',
145
- Inches = 'in',
146
- FractionalInches = 'in_frac',
147
- Feet = 'ft',
148
- FeetInchesDecimal = 'ft_in_decimal',
149
- FeetInchesFractional = 'ft_in_frac',
150
- }
151
-
152
- export const convertUnitsToReadable = (targetUnit: Units) => {
153
- switch (targetUnit) {
154
- case Units.Meters:
155
- return 'm';
156
- case Units.Millimeters:
157
- return 'mm';
158
- case Units.Centimeters:
159
- return 'cm';
160
- case Units.Feet:
161
- return 'ft';
162
- case Units.FractionalInches:
163
- return 'in (fractional)';
164
- case Units.Inches:
165
- return 'in';
166
- case Units.FeetInchesDecimal:
167
- return 'ft-in (decimal)';
168
- case Units.FeetInchesFractional:
169
- return 'ft-in (fractional)';
170
- default:
171
- return null;
172
- }
173
- };
174
-
175
- export enum FractionalTolerance {
176
- Fourth = '4',
177
- Eighth = '8',
178
- Sixteenth = '16',
179
- ThirtySecond = '32',
180
- SixtyFourth = '64',
181
- OneTwentyEighth = '128',
182
- }
183
-
184
- export enum DecimalTolerance {
185
- Half = '0.5',
186
- Tenth = '0.1',
187
- Hundredth = '0.01',
188
- Thousandth = '0.001',
189
- }
190
-
191
- export interface UserDocument extends FirestoreDoc, Timestamps {
192
- defaultOrganization: string;
193
- displayName: string;
194
- email: string;
195
- firstName: string;
196
- lastName: string;
197
- defaultUnit: Units;
198
- decimalTolerance: DecimalTolerance;
199
- fractionalTolerance: FractionalTolerance;
200
- showWizard: boolean; // The onboarding screen displayed on web
201
- printLabelSize: string;
202
- };
@@ -1,57 +0,0 @@
1
- import { create, all, Unit } from 'mathjs';
2
-
3
- const math = create(all);
4
-
5
- type MeasurementMap = Map<string, { id: string; value: number }[]>;
6
-
7
- interface FormulaEvaluationOptions {
8
- expression: string;
9
- mappings: Record<string, string>;
10
- group: { id: string; columns: Map<string, any> };
11
- columns: { id: string; type: string }[];
12
- measurementMap: MeasurementMap;
13
- defaultUnit?: string; // 'um' by default
14
- }
15
-
16
- export function evaluateFormula({
17
- expression,
18
- mappings,
19
- group,
20
- columns,
21
- measurementMap,
22
- defaultUnit = 'um',
23
- }: FormulaEvaluationOptions): number | string {
24
- const scope: Record<string, any> = {};
25
-
26
- for (const [variable, columnId] of Object.entries(mappings)) {
27
- const rawValue = group.columns.get(columnId);
28
- const columnDef = columns.find((c) => c.id === columnId);
29
-
30
- if (columnDef?.type === 'Measurement') {
31
- const groupMeasurements = measurementMap.get(group.id) || [];
32
- const measurement = groupMeasurements.find((m) => m.id === rawValue);
33
- if (measurement?.value !== undefined) {
34
- scope[variable] = math.unit(measurement.value, defaultUnit);
35
- continue;
36
- }
37
- }
38
-
39
- const parsedValue =
40
- typeof rawValue === 'number' ? rawValue : parseFloat(rawValue || '');
41
- scope[variable] = isNaN(parsedValue) ? 0 : parsedValue;
42
- }
43
-
44
- try {
45
- const compiled = math.compile(expression);
46
- const result = compiled.evaluate(scope);
47
-
48
- if (math.typeOf(result) === 'Unit') {
49
- return result.toNumber(defaultUnit);
50
- }
51
-
52
- return result;
53
- } catch (err) {
54
- console.error(`Formula evaluation failed:`, err);
55
- return 'Error';
56
- }
57
- }
@@ -1,75 +0,0 @@
1
- import { convertUnitsToReadable, DecimalTolerance, FractionalTolerance, Units } from '../index';
2
- import { create, all } from 'mathjs';
3
-
4
- const math = create(all);
5
-
6
- export const convertMicrometers = (
7
- micrometers: number,
8
- unit: Units,
9
- fractionalTolerance: FractionalTolerance,
10
- decimalTolerance: DecimalTolerance
11
- ): { value: string; unit: string | null } => {
12
- const converted = math.unit(micrometers, 'um');
13
- let value: number | string = 0;
14
- let displayUnit: string | null = '';
15
-
16
- switch (unit) {
17
- case Units.Inches:
18
- case Units.FractionalInches: {
19
- const inches = converted.toNumber('in');
20
- displayUnit = 'in';
21
- if (unit === Units.FractionalInches) {
22
- const denominator = parseInt(fractionalTolerance, 10);
23
- const whole = Math.floor(inches);
24
- const fractional = inches - whole;
25
- const numerator = Math.round(fractional * denominator);
26
-
27
- value =
28
- numerator === 0
29
- ? `${whole}`
30
- : `${whole} ${numerator}/${denominator}`;
31
- } else {
32
- value = inches.toFixed(decimalTolerance.length - 2); // Match decimal places to tolerance
33
- }
34
- break;
35
- }
36
- case Units.Feet:
37
- case Units.FeetInchesFractional:
38
- case Units.FeetInchesDecimal: {
39
- const feet = converted.toNumber('ft');
40
- const wholeFeet = Math.floor(feet);
41
- const fractionalFeet = feet - wholeFeet;
42
- displayUnit = 'ft';
43
-
44
- if (unit === Units.FeetInchesFractional) {
45
- const inches = fractionalFeet * 12;
46
- const wholeInches = Math.floor(inches);
47
- const fractional = inches - wholeInches;
48
- const denominator = parseInt(fractionalTolerance, 10);
49
- const numerator = Math.round(fractional * denominator);
50
-
51
- value =
52
- numerator === 0
53
- ? `${wholeFeet}' ${wholeInches}"`
54
- : `${wholeFeet}' ${wholeInches} ${numerator}/${denominator}"`;
55
- } else if (unit === Units.FeetInchesDecimal) {
56
- const inches = (fractionalFeet * 12).toFixed(decimalTolerance.length - 2);
57
- value = `${wholeFeet}' ${inches}"`;
58
- } else {
59
- value = feet.toFixed(decimalTolerance.length - 2);
60
- }
61
- break;
62
- }
63
- case Units.Meters:
64
- case Units.Centimeters:
65
- case Units.Millimeters: {
66
- displayUnit = convertUnitsToReadable(unit);
67
- value = converted.toNumber(unit).toFixed(decimalTolerance.length - 2);
68
- break;
69
- }
70
- default:
71
- throw new Error('Unsupported unit');
72
- }
73
-
74
- return { value, unit: displayUnit };
75
- };
@@ -1,54 +0,0 @@
1
- import { all, create } from 'mathjs';
2
-
3
- const math = create(all);
4
-
5
- export const parseMeasurement = (
6
- input: string,
7
- defaultUnit: string = 'mm'
8
- ): number | null => {
9
- try {
10
- // Trim and normalize spaces
11
- const normalizedInput = input.trim().replace(/\s+/g, ' ');
12
-
13
- // Preprocess mixed fractions (e.g., "12 1/2" -> "12 + 1/2")
14
- const processedInput = normalizedInput.replace(
15
- /(\d+)\s+(\d+\/\d+)/g,
16
- '$1 + $2'
17
- );
18
-
19
- // Match numeric value and optional unit
20
- const match = processedInput.match(/^([\d.+\-*/\s]+)\s*(\w+)?$/);
21
-
22
- if (!match) {
23
- throw new Error('Invalid input format');
24
- }
25
-
26
- const [_, value, unit] = match;
27
-
28
- // Evaluate the numeric value (e.g., "12 + 1/2")
29
- const numericValue = math.evaluate(value.trim());
30
-
31
- // Use the provided unit or fallback to the default unit
32
- const targetUnit = unit || defaultUnit;
33
-
34
- // Convert to micrometers
35
- return math.unit(numericValue, targetUnit).toNumber('um');
36
- } catch (err) {
37
- if (err instanceof Error) {
38
- console.error('Invalid measurement:', err.message);
39
- } else {
40
- console.error('An unknown error occurred:', err);
41
- }
42
- return null;
43
- }
44
- };
45
-
46
- export const numberToLetterIndex = (index: number): string => {
47
- let result = '';
48
- while (index > 0) {
49
- index--; // Adjust for 0-based index
50
- result = String.fromCharCode((index % 26) + 65) + result;
51
- index = Math.floor(index / 26);
52
- }
53
- return result;
54
- };