Skip to content

Commit

Permalink
Dimensional v0.1.0 (#98)
Browse files Browse the repository at this point in the history
Basic exports for dimensions and a few units.
  • Loading branch information
nicfv authored Apr 11, 2024
1 parent 8ba6ab1 commit 5e67a31
Show file tree
Hide file tree
Showing 11 changed files with 409 additions and 9 deletions.
7 changes: 7 additions & 0 deletions dimensional/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.1.0

- Install relevant dependencies
- Add base classes for compounds, dimensions, and units
- Add unit tests for dimensions and units
- Units are associated with base dimensions from a conversion table

## 0.0.0

- Initialize empty package
2 changes: 1 addition & 1 deletion dimensional/examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"clean": "rm -rf node_modules package-lock.json"
},
"dependencies": {
"dimensional": "file:dimensional-0.0.0.tgz"
"dimensional": "file:dimensional-0.1.0.tgz"
}
}
12 changes: 8 additions & 4 deletions dimensional/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dimensional",
"version": "0.0.0",
"version": "0.1.0",
"description": "Dimensional analysis and unit conversions",
"homepage": "https://npm.nicfv.com/dimensional",
"bin": "",
Expand All @@ -14,7 +14,7 @@
"build": "rm -rf dist types && tsc && node dist/test.js && rm dist/test.js",
"test": "tsc --noEmit",
"clean": "rm -rf node_modules package-lock.json dist types docs",
"docs": "rm -rf docs && typedoc --includeVersion --disableSources --hideGenerator src",
"docs": "rm -rf docs && typedoc --includeVersion --disableSources --hideGenerator --excludePrivate --excludeProtected src",
"prepack": "npm run build",
"postpack": "rm -rf dist types"
},
Expand All @@ -30,8 +30,12 @@
},
"repository": "github:nicfv/npm",
"license": "MIT",
"dependencies": {
"smath": "1.8.1"
},
"devDependencies": {
"typedoc": "0.25.12",
"typescript": "5.4.4"
"exray": "1.0.3",
"typedoc": "0.25.13",
"typescript": "5.4.5"
}
}
126 changes: 126 additions & 0 deletions dimensional/src/compound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { SMath } from 'smath';
import { NumberDictionary } from './lib';

/**
* Represents a compound unit or dimension.
*/
export abstract class Compound<T extends string, C extends Compound<T, C>> {
private readonly num: NumberDictionary<T> = {};
private readonly den: NumberDictionary<T> = {};
/**
* Create a new compound unit or dimension.
* @param exponents Contains an object of exponent values
* @param toLaTeX A function to convert the exponent to its representation in LaTeX
*/
constructor(private readonly exponents: NumberDictionary<T>, private readonly toLaTeX: (exponent: T) => string) {
for (const t in exponents) {
const exponent: number = this.getExponent(t);
if (exponent > 0) {
this.num[t] = exponent;
} else if (exponent < 0) {
this.den[t] = -exponent;
}
}
}
/**
* Combine two compounds by applying a factor on the second compound.
* @param other Another compount
* @param factor The factor to use on the other compound
* @returns The combination of the two compounds
*/
protected combine(other: C, factor: number): NumberDictionary<T> {
const exponents_combined: NumberDictionary<T> = {};
for (const t in this.exponents) {
exponents_combined[t] = this.getExponent(t);
}
for (const t in other.exponents) {
exponents_combined[t] = this.getExponent(t) + factor * other.getExponent(t);
}
return exponents_combined;
}
/**
* Multiply this compound by another after applying an exponent on the second compound.
* @param other Another compound fraction
* @param exponent The exponent to apply on the other compound
* @returns The product of the two compounds
*/
public abstract mult(other: C, exponent: number): C;
/**
* Determine whether two compounds contain the same units or dimensions.
* @param other Another compound
* @returns A boolean
*/
public is(other: C): boolean {
const dividend: NumberDictionary<T> = this.combine(other, -1);
for (let t in dividend) {
if (!SMath.approx(dividend[t] ?? 0, 0)) {
return false;
}
}
return true;
}
/**
* Determine the exponent on the specified unit or dimension.
* @param exponent The unit or dimension to retrieve the exponent from
* @returns The exponent
*/
public getExponent(exponent: T): number {
return this.exponents[exponent] ?? 0;
}
/**
* Generate an array of nonzero exponent units or dimensions.
* @returns An array of nonzero exponent units or dimensions
*/
public getNonzeroExponents(): Array<T> {
const nonzeroExponents: Array<T> = [];
for (const t in this.exponents) {
if (this.getExponent(t)) {
nonzeroExponents.push(t);
}
}
return nonzeroExponents;
}
/**
* Generate LaTeX code for a number dictionary.
* @param dict Any number dictionary
* @returns Partial LaTeX code
*/
private prettyPrint(dict: NumberDictionary<T>): string {
let str: string = '';
for (const t in dict) {
if (str.length) {
str += ' \\cdot ';
}
const exponent: number = dict[t] ?? 0;
if (SMath.approx(exponent, 1)) {
str += this.toLaTeX(t);
} else if (SMath.approx(exponent, 0.5)) {
str += '\\sqrt{' + this.toLaTeX(t) + '}';
} else {
str += this.toLaTeX(t) + '^{' + exponent.toString() + '}';
}
}
return str;
}
/**
* Generate valid LaTeX code representing this compound.
* @returns A valid LaTeX equation
*/
public toString(): string {
let str: string = '';
const hasNum: boolean = Object.keys(this.num).length > 0,
hasDen: boolean = Object.keys(this.den).length > 0;
if (hasDen) {
str += '\\frac{';
}
if (hasNum) {
str += this.prettyPrint(this.num);
} else {
str += '1';
}
if (hasDen) {
str += '}{' + this.prettyPrint(this.den) + '}';
}
return str;
}
}
125 changes: 125 additions & 0 deletions dimensional/src/conversion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Dim, Dimension } from './dimension';
import { Units } from './unit';

/**
* Contains information on how units should be converted.
*/
export interface Conversion {
/**
* The LaTeX representation of this unit.
*/
readonly latex: string;
/**
* The base physical dimensions of this unit.
*/
readonly dim: Dimension;
/**
* The scale of this unit in relation to the base unit of this dimension.
*/
readonly scale: number;
}
/**
* Represents the full conversion table for **absolute** units only.
*/
export const ConversionTable: { [index in Units]: Conversion } = {
'centimeters': {
latex: 'cm',
dim: Dim({ length: 1 }),
scale: 1e-2,
},
'days': {
latex: 'd',
dim: Dim({ time: 1 }),
scale: 60 * 60 * 24,
},
'feet': {
latex: 'ft',
dim: Dim({ length: 1 }),
scale: 0.3048,
},
'hours': {
latex: 'h',
dim: Dim({ time: 1 }),
scale: 60 * 60,
},
'inches': {
latex: 'in',
dim: Dim({ length: 1 }),
scale: 0.3048 / 12,
},
'kilometers': {
latex: 'km',
dim: Dim({ length: 1 }),
scale: 1e3,
},
'meters': {
latex: 'm',
dim: Dim({ length: 1 }),
scale: 1,
},
'micrometers': {
latex: '\\mu m',
dim: Dim({ length: 1 }),
scale: 1e-6,
},
'microns': {
latex: '\\mu m',
dim: Dim({ length: 1 }),
scale: 1e-6,
},
'miles': {
latex: 'mi',
dim: Dim({ length: 1 }),
scale: 0.3048 * 5280,
},
'millimeters': {
latex: 'mm',
dim: Dim({ length: 1 }),
scale: 1e-3,
},
'milliseconds': {
latex: 'ms',
dim: Dim({ time: 1 }),
scale: 1e-3,
},
'minutes': {
latex: 'm',
dim: Dim({ time: 1 }),
scale: 60,
},
'months': {
latex: 'M',
dim: Dim({ time: 1 }),
scale: 60 * 60 * 24 * 365.25 / 12,
},
'nanometers': {
latex: 'nm',
dim: Dim({ length: 1 }),
scale: 1e-9,
},
'nanoseconds': {
latex: 'ns',
dim: Dim({ time: 1 }),
scale: 1e-9,
},
'seconds': {
latex: 's',
dim: Dim({ time: 1 }),
scale: 1,
},
'weeks': {
latex: 'w',
dim: Dim({ time: 1 }),
scale: 60 * 60 * 24 * 7,
},
'yards': {
latex: 'yd',
dim: Dim({ length: 1 }),
scale: 0.3048,
},
'years': {
latex: 'y',
dim: Dim({ time: 1 }),
scale: 60 * 60 * 24 * 365.25,
},
};
38 changes: 38 additions & 0 deletions dimensional/src/dimension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Compound } from './compound';
import { Dictionary, NumberDictionary } from './lib';

/**
* A list of common names for each of the physical base dimensions.
*/
export type Dimensions = 'time' | 'length' | 'mass' | 'current' | 'temperature' | 'amount' | 'intensity';
/**
* Contains all physical base dimensions and their corresponding abbreviations.
*/
const DimensionTable: Dictionary<Dimensions> = {
'amount': '\\textbf{N}',
'current': '\\textbf{I}',
'intensity': '\\textbf{J}',
'length': '\\textbf{L}',
'mass': '\\textbf{M}',
'temperature': '\\boldsymbol{\\Theta}',
'time': '\\textbf{T}',
};
/**
* Defines the class for physical base dimensions.
*/
export class Dimension extends Compound<Dimensions, Dimension> {
constructor(exponents: NumberDictionary<Dimensions>) {
super(exponents, t => DimensionTable[t]);
}
public mult(other: Dimension, exponent: number): Dimension {
return new Dimension(super.combine(other, exponent));
}
}
/**
* Shorthand for creating a dimension object.
* @param exponents Exponents of each of the physical base dimensions
* @returns A new dimension
*/
export function Dim(exponents: NumberDictionary<Dimensions>): Dimension {
return new Dimension(exponents);
}
4 changes: 3 additions & 1 deletion dimensional/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
* Dimensional analysis and unit conversions
*
* Exports the public-facing API for `dimensional`
*/
*/
export * from './dimension';
export * from './unit';
8 changes: 8 additions & 0 deletions dimensional/src/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Represents a type where every object value is a string.
*/
export type Dictionary<T extends string> = { [index in T]: string };
/**
* Represents a type where every object value is a number.
*/
export type NumberDictionary<T extends string> = { [index in T]?: number };
26 changes: 26 additions & 0 deletions dimensional/src/quantity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SMath } from 'smath';
import { ConversionTable } from './conversion';
import { Unit } from './unit';

export class Quantity {
private readonly base: number;
constructor(private readonly value: number, private readonly unit: Unit) {
this.base = value;
for (const u of unit.getNonzeroExponents()) {
this.base = SMath.expand(this.base, 0, ConversionTable[u].scale ** unit.getExponent(u));
}
}
public as(newUnit: Unit): Quantity {
if (!this.unit.dimension.is(newUnit.dimension)) {
throw new Error('\\text{Dimensions do not match! } ' + this.unit.dimension.toString() + ' \\text{ vs. } ' + newUnit.dimension.toString())
}
let newValue: number = this.base;
for (const u of newUnit.getNonzeroExponents()) {
newValue = SMath.normalize(newValue, 0, ConversionTable[u].scale ** newUnit.getExponent(u));
}
return new Quantity(newValue, newUnit);
}
public toString(): string {
return this.value.toString() + '\\left[' + this.unit.toString() + '\\right]';
}
}
Loading

0 comments on commit 5e67a31

Please sign in to comment.