js-quantities icon indicating copy to clipboard operation
js-quantities copied to clipboard

Human readable value in contrained range

Open vallsv opened this issue 3 years ago • 2 comments

Hi,

Thanks a lot for that library. Looks very useful for now.

I would like to know the best way to pick a "best" unit for human readable value.

I have to deal with metrics of about mm/um/nm` so in my case i can check easially what the best unit i can pick.

But for the general use case, what would be the best approach to render a value between 0.1 < < 999, by picking the appropriate prefix p/n/u/m//k/M/G to match the constraint? Looking at the documentation i really have no idea.

Thanks a lot.

vallsv avatar Oct 15 '21 10:10 vallsv

I did some work around.

Here is an implementation outside of the lib.

It is probably very specific use case, but if you would like to provide on your side a shiftUnit and something like getUnitPlace, it would be very useful to reduce the size of such code.

If you like that idea, i can create a pull request.

const PREFIX = {
  '<tera>': 12,
  '<giga>': 9,
  '<mega>': 6,
  '<kilo>': 3,
  '<1>': 0,
  '<milli>': -3,
  '<micro>': -6,
  '<nano>': -9
};

const PLACES_TO_PREFIX = {};

for (const [key, value] of Object.entries(PREFIX)) {
  PLACES_TO_PREFIX[value] = key;
}

/**
 * Returns a quantity with a unit inside a readable range of values
 *
 * The prefix of the unit (n/u/k/M) is picked to make the value
 * in the range 0.1 .. 999
 */
export function toHumanReadableUnit(quantity) {
  if (quantity.scalar === 0) {
    return quantity;
  }

  function getNextDeltaPlace(q) {
    const v = q.scalar;
    if (v > 200) {
      return 3;
    }
    if (v < 0.3) {
      return -3;
    }
    return 0;
  }

  function getUnitWithNewPrefix(q, prefix) {
    function cleanup(elemString) {
      if (elemString === undefined) return '';
      if (elemString === '<1>') return '';
      return elemString.substring(1, elemString.length - 1);
    }
    let n = '';
    for (let ni = 0; ni < q.numerator.length; ni += 1) {
      const elem = q.numerator[ni];
      if (ni === 0) {
        const notPrefix = PREFIX[elem] === undefined;
        n += cleanup(prefix) + (notPrefix ? cleanup(elem) : '');
      } else {
        n += cleanup(elem);
      }
    }
    let d = '';
    for (let di = 0; di < q.denominator.length; di += 1) {
      const elem = q.denominator[di];
      d += cleanup(elem);
    }
    if (d === '') {
      return n;
    }
    if (n === '') {
      return `1/${d}`;
    }
    return `${n}/${d}`;
  }

  function getCurrentPlace(q) {
    return PREFIX[q.numerator[0]] || 0;
  }

  function shiftUnit(q, deltaPlace) {
    const place = getCurrentPlace(q) + deltaPlace;
    const prefixNumerator = PLACES_TO_PREFIX[place];
    if (prefixNumerator === undefined) {
      return q;
    }
    const unit = getUnitWithNewPrefix(quantity, prefixNumerator);
    return q.to(unit);
  }

  let current = quantity;
  for (let i = 0; i < 10; i += 1) {
    const deltaPlace = getNextDeltaPlace(current);
    if (deltaPlace === 0) {
      break;
    }
    current = shiftUnit(current, deltaPlace);
  }

  // Check if we loose a bit of digits
  const fixed = current.scalar.toFixed(2);
  if (
    fixed.length === 4 &&
    fixed.substring(0, 2) === '0.' &&
    fixed.substring(3, 4) !== '0'
  ) {
    return shiftUnit(current, -3);
  }
  return current;
}
import Qty from 'js-quantities';
import { toHumanReadableUnit } from 'helpers/QtyHelper';

describe('Qty helper', () => {
  test('check already normalized', async () => {
    const q = Qty('10 mm');
    const result = toHumanReadableUnit(q);
    expect(result.toPrec(0.1).toString()).toEqual('10 mm');
  });
  test('check with smaller unit than expected', async () => {
    const q = Qty('1000 mm');
    const result = toHumanReadableUnit(q);
    expect(result.toPrec(0.1).toString()).toEqual('1 m');
  });
  test('check with bigger unit than expected', async () => {
    const q = Qty('0.02 mm');
    const result = toHumanReadableUnit(q);
    expect(result.toPrec(0.1).toString()).toEqual('20 um');
  });
  test('check with smaller unit than expected', async () => {
    const q = Qty('0.0222 mm');
    const result = toHumanReadableUnit(q);
    expect(result.toPrec(0.1).toString()).toEqual('22.2 um');
  });
  test('check with more than 2 precision digit', async () => {
    const q = Qty('0.222 mm');
    const result = toHumanReadableUnit(q);
    expect(result.toPrec(0.1).toString()).toEqual('222 um');
  });
  test('check with a value bigger than 200', async () => {
    const q = Qty('0.500 mm');
    const result = toHumanReadableUnit(q);
    expect(result.toPrec(0.1).toString()).toEqual('0.5 mm');
  });
  test('check above rounding error', async () => {
    const q = Qty('0.995 mm');
    const result = toHumanReadableUnit(q);
    expect(result.toPrec(0.1).toString()).toEqual('995 um');
  });
  test('check under rounding error', async () => {
    const q = Qty('0.996 mm');
    const result = toHumanReadableUnit(q);
    expect(result.toPrec(0.1).toString()).toEqual('1 mm');
  });
});

vallsv avatar Oct 15 '21 13:10 vallsv

Duplicate of #87 ?

adrfantini avatar Nov 22 '23 08:11 adrfantini