Skip to content

Commit

Permalink
Dimensional v0.2.0 (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicfv authored Apr 13, 2024
1 parent ce834a7 commit 0e46b9f
Show file tree
Hide file tree
Showing 17 changed files with 686 additions and 332 deletions.
16 changes: 15 additions & 1 deletion dimensional/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 0.2.0

- Update dependency versions
- Remove `docs/` on postpack
- Scope everything using namespaces
- Add unit conversions for several units
- Add `None` types for dimensionless and unitless quantities
- Export helper functions `D()`, `U()`, and `Q()` for shorthand creation of dimensions, units, and quantities
- Create internal methods for metric prefixes and measurement types
- Unit scaling is now computed in the `Unit` class instead of `Quantity`
- Add several unit tests
- Add two basic examples

## 0.1.1

- Use global TypeDoc configuration file
Expand All @@ -18,4 +31,5 @@

## 0.0.0

- Initialize empty package
- Initialize empty package
- No longer available on npm
7 changes: 7 additions & 0 deletions dimensional/examples/1-Use-The-Force.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
One day, you step onto a scale to determine your weight. It reads the number **150**. What is this number? It is the **force** you are applying on to the scale. Europeans would be terrified to see this number, but Americans wouldn't even think twice. See, in the United States, scales read out units of **pounds** where in Europe, it would read out units of **kilograms**. What would a European scale say your weight is?

Now, here's an interesting predicament - pounds are units of **force** whereas kilograms are units of **mass**, which are completely different **dimensions**. In reality, a European scale is also measuring the force you apply, but in a different unit, called **Newtons**.

> It just so happens that pounds are *also* units of mass, and on Earth, one pound of force equals one pound of mass. We'll get to this later.
So now that we know a European scale is actually measuring Newtons, what value would that be for \\\(150 [lb]\\\)? Based on results from plugging it into an online conversion calculator, I expect to see around \\\(667 [N]\\\). Let's see if it checks out.
16 changes: 16 additions & 0 deletions dimensional/examples/1-Use-The-Force.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Q, U } from 'dimensional';

// Weight is actually a force - not a mass!
// Therefore, units must be in pounds of force
const weight_lbs = Q(150, U({ pound_force: 1 }));

// We can easily obtain our weight in Newtons
// with a simple conversion using Quantity.as(unit)
const weight_N = weight_lbs.as(U({ Newton: 1 }));

// Print out the results of the conversion
console.log(weight_lbs.toString() + '=' + weight_N.toString());

// In case we want the raw value...
const weight_N_value = weight_N.value;
console.log('Raw value = ' + weight_N_value);
19 changes: 19 additions & 0 deletions dimensional/examples/2-Quantity-Math.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Great, our first example checks out! But like I mentioned, European scales don't actually read out in Newtons, but in kilograms. Remember the relationship between force and mass from your physics class?

$$F=ma$$

$$\text{Force}=\text{mass}\times\text{acceleration}$$

We know the value of force and want to solve for mass. What would the acceleration be? This is the acceleration due to Earth's gravity. A good way to visualize that number is drop something (light weight, be safe!) and watch it fall. Notice how it speeds up as it falls - the "speed up" is the acceleration. We call this value \\\(g\\\). On Earth, \\\(g\approx 9.81 [m/s^{2}]\\\) in SI units.

$$m=\frac{F}{g}$$

We could convert \\\(g\\\) to US units, but there's no need, since `dimensional` will handle all unit conversions internally.

When we divide force by acceleration, the units from force and acceleration will persist. We'll end up with a rather strange unit like this:

$$\frac{lb_{f}}{\frac{m}{s^{2}}}=\frac{lb_{f}s^{2}}{m}$$

What are the dimensions on this unit? We can run a quick dimensional analysis using `Quantity.Unit.Dimension.toString()` to get a human-readable representation of our physical base dimensions. Believe it or not, the dimension of that unit is just mass! That means, we can convert quantities with that unit to any unit of mass.

From plugging it into an online calculator, I expect the result to be about \\\(68 [kg]\\\).
20 changes: 20 additions & 0 deletions dimensional/examples/2-Quantity-Math.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Q, U } from 'dimensional';

// This is our weight from the previous example
const weight_lbs = Q(150, U({ pound_force: 1 }));

// Define Earth's gravity at sea level in SI units
const gravity = Q(9.81, U({ meter: 1, second: -2 }));

// Remember `F=ma`? Rearrange to `m=F/a`
const mass = weight_lbs.over(gravity);

// The units persist through the operation, unless if they cancel
// So we'll get `lbf*s^2/m` ... which is not the most useful unit
console.log(mass.toString());

// What are the dimensions on this weird unit?
console.log('dim=' + mass.unit.dimension.toString());

// We can use the Quantity.as(unit) method to convert to kg
console.log(mass.as(U({ kilogram: 1 })).toString());
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.1.1.tgz"
"dimensional": "file:dimensional-0.2.0.tgz"
}
}
12 changes: 6 additions & 6 deletions dimensional/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dimensional",
"version": "0.1.1",
"version": "0.2.0",
"description": "Dimensional analysis and unit conversions",
"homepage": "https://npm.nicfv.com/dimensional",
"bin": "",
Expand All @@ -11,11 +11,11 @@
"types"
],
"scripts": {
"build": "rm -rf dist types docs && typedoc --options ../typedoc.json src && node dist/test.js && rm dist/test.js types/test.d.ts",
"build": "npm run postpack && typedoc --options ../typedoc.json src && node dist/test.js && rm dist/test.js types/test.d.ts",
"test": "tsc -v && tsc --noEmit",
"clean": "rm -rf node_modules package-lock.json dist types docs",
"clean": "rm -rf node_modules package-lock.json && npm run postpack",
"prepack": "npm run build",
"postpack": "rm -rf dist types"
"postpack": "rm -rf dist types docs"
},
"keywords": [],
"author": {
Expand All @@ -30,10 +30,10 @@
"repository": "github:nicfv/npm",
"license": "MIT",
"dependencies": {
"smath": "1.8.1"
"smath": "1.8.2"
},
"devDependencies": {
"exray": "1.0.3",
"exray": "1.0.4",
"typedoc": "0.25.13"
}
}
221 changes: 114 additions & 107 deletions dimensional/src/compound.ts
Original file line number Diff line number Diff line change
@@ -1,126 +1,133 @@
import { SMath } from 'smath';
import { NumberDictionary } from './lib';

/**
* Represents a compound unit or dimension.
* Contains all software used for the calculation of compound values.
*/
export abstract class Compound<T extends string, C extends Compound<T, C>> {
private readonly num: NumberDictionary<T> = {};
private readonly den: NumberDictionary<T> = {};
export namespace Compound {
/**
* 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
* Represents a type where every object value is a number representing exponents.
*/
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;
}
}
}
export type Exponents<T extends string> = { [index in T]?: number };
/**
* 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
* Represents a compound unit or dimension.
*/
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);
export abstract class Compound<T extends string, C extends Compound<T, C>> {
private readonly num: Exponents<T> = {};
private readonly den: Exponents<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: Exponents<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;
}
}
}
for (const t in other.exponents) {
exponents_combined[t] = this.getExponent(t) + factor * other.getExponent(t);
/**
* 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): Exponents<T> {
const exponents_combined: Exponents<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;
}
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;
/**
* 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: Exponents<T> = this.combine(other, -1);
for (let t in dividend) {
if (!SMath.approx(dividend[t] ?? 0, 0)) {
return false;
}
}
return true;
}
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);
/**
* 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;
}
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 ';
/**
* Generate LaTeX code for an exponents object.
* @param expo Any exponents object
* @returns Partial LaTeX code
*/
private prettyPrint(expo: Exponents<T>): string {
let str: string = '';
for (const t in expo) {
if (str.length) {
str += ' \\cdot ';
}
const exponent: number = expo[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{';
}
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) + '}';
if (hasNum) {
str += this.prettyPrint(this.num);
} else {
str += this.toLaTeX(t) + '^{' + exponent.toString() + '}';
str += '1';
}
if (hasDen) {
str += '}{' + this.prettyPrint(this.den) + '}';
}
return str;
}
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;
}
}
Loading

0 comments on commit 0e46b9f

Please sign in to comment.