bbc/unicode-bidirectional

View on GitHub
src/resolve/reorderedLevels.js

Summary

Maintainability
A
0 mins
Test Coverage
import { List, Map, Range, Record } from 'immutable';
import isNumber from 'lodash.isnumber';

const ReorderPair = Record({ level: -1, from: 0, to: 0 }, 'ReorderPair');

const spliceList = (list, from, to, paste) => {
  const left = list.slice(0, from);
  const right = list.slice(to);
  return left.concat(paste).concat(right);
};

// Returns the storage -> display reordering performed by UBA
// representing as a permutation on the set {0, 1, 2, ... n - 1}
// Permutation represented via "one-line notation"
// see: https://en.wikipedia.org/wiki/Permutation#Definition_and_notations
function reorderPermutation(levels, IGNORE_INVISIBLE = false, INVISIBLE_MARK = 'x') {
  const storage = List(Range(0, levels.size))
    .map(i => Map({ strip: levels.get(i) === INVISIBLE_MARK, index: i }))
    .filter(x => x.get('strip') === false)
    .map(x => x.get('index'));

  const levelsWithoutInvisibles = levels.filter(x => x != INVISIBLE_MARK);
  const reorderedWithoutInvisibles = reorderedLevels(storage, levelsWithoutInvisibles);

  const Reduction = Record({ remaining: List(), result: List() }, 'Reduction');
  const initialReduction = new Reduction({ remaining: reorderedWithoutInvisibles, result: List() });
  const permutation = List(Range(0, levels.size))
    .reduce((reduction, i) => {

      if (levels.get(i) == INVISIBLE_MARK) {
        const j = reduction.get('result').size;
        return reduction.setIn(['result', i], j);
      }

      const remaining = reduction.get('remaining');
      return reduction
        .setIn(['result', i], remaining.first())
        .set('remaining', remaining.shift());
    }, initialReduction)
    .get('result');

  if (IGNORE_INVISIBLE) {
    return reorderedWithoutInvisibles;
  } else {
    return permutation;
  }
}

// http://www.unicode.org/reports/tr9/#L2
// first:   reverse slices at levels:  max
// then:    reverse slices at levels:  max,max-1
// then:    reverse slices at levels:  max,max-1,max-2
// ...
// finally: reverse slices at levels:  max,max-1,max-2,...,1
function reorderedLevels(storage, levels) {
  const slicesByLevel = reorderingSlices(levels, 0)
    .groupBy(slice => slice.get('level'));

  const maxLevel = slicesByLevel.keySeq().max();
  if (!isNumber(maxLevel) || maxLevel < 0) {
    return storage;
  }

  if (maxLevel === 0) {
    return storage;
  } else {
    const slices = slicesByLevel.get(maxLevel);

    const storageAfter = slices.reduce((curr, slice) => {
      const { from, to } = slice.toJS();
      const reversed = curr.slice(from, to).reverse();
      return spliceList(curr, from, to, reversed);
    }, storage)

    const levelsAfter = slices.reduce((curr, slice) => {
      const { from, to } = slice.toJS();
      const nextLevel = List(Range(0, to - from)).map(__ => maxLevel - 1);
      return spliceList(curr, from, to, nextLevel);
    }, levels)

    return reorderedLevels(storageAfter, levelsAfter);
  }
}

function reorderingSlices(levels, offset) {
  const N = levels.size;
  if (N === 0) return List.of();

  const level = levels.first();
  const nextLevelIndex = levels.findKey(v => v != level);
  const size = (nextLevelIndex === undefined) ? N : nextLevelIndex;
  const slice = new ReorderPair({ level, from: offset, to: offset + size });

  return List.of(slice)
    .concat(reorderingSlices(levels.slice(size), offset + size));
}

export { reorderPermutation };
export default reorderedLevels;