AutoScheduleJS/userstate-manager

View on GitHub
src/data-flows/main.flow.ts

Summary

Maintainability
A
55 mins
Test Coverage
import { IQueryInternal, IQueryTransformationInternal } from '@autoschedule/queries-fn';
import { intersect, simplify } from 'intervals-fn';
import * as loki from 'lokijs';
import { groupWith, prop, sortBy, unnest } from 'ramda';
import { IConfig } from '../data-structures/config.interface';
import { IIdentifier } from '../data-structures/identifier.interface';
import { IGroupNeedResource, INeedResource } from '../data-structures/need-resource.interface';
import { IRangeNeedSatisfaction } from '../data-structures/need-satisfaction.interface';
import {
  IMaterial,
  IPotentiality,
  IPotRange,
  IRange,
} from '../data-structures/queries-scheduler.interface';
import { IRefDoc } from '../data-structures/ref-doc.interface';
import { ITransformSatisfaction } from '../data-structures/transform-satisfaction.interface';
import { allTransfo, ITransformationTime } from '../data-structures/transformation-time.interface';
import { IUserstateCollection } from '../data-structures/userstate-collection.interface';
import { computeOutputSatisfaction, computeRangeSatisfaction } from './satisfactions.flow';

type IQuery = IQueryInternal;

const objectDBToDB = (objectDB: ReadonlyArray<IUserstateCollection>): Loki => {
  const db = new loki('simul');
  objectDB.forEach(userStateCol => {
    const col = db.addCollection(userStateCol.collectionName);
    col.insert(userStateCol.data);
  });
  return db;
};

const configToRange = (config: IConfig) => ({ start: config.startDate, end: config.endDate });
const configToTransforanges = (config: IConfig): ITransformationTime[] => [
  { deletes: [], time: config.endDate, inserts: [], needs: [], updates: [] },
  { deletes: [], time: config.startDate, inserts: [], needs: [], updates: [] },
];
const rangeSatisEligible = (rangeSat: IRangeNeedSatisfaction): boolean =>
  rangeSat.needSatisfactions.every(sat => sat.satisfied);

const useSameResources = (a: INeedResource, b: INeedResource): boolean => {
  return (
    a.id.query === b.id.query &&
    a.id.potential === b.id.potential &&
    a.id.material === b.id.material &&
    a.id.split === b.id.split
  );
};

const initialGroupNR: IGroupNeedResource = {
  collectionName: '',
  find: {},
  ids: [],
  missing: 0,
  missingTime: [],
  quantity: 0,
  ref: '',
};

const groupNeedResources = (needResources: INeedResource[]): IGroupNeedResource[] => {
  return groupWith<INeedResource>(useSameResources)(sortByMissingTime(needResources)).map(needRes =>
    needRes.reduce(
      (a, b) => ({
        ...b,
        ids: [...a.ids, b.id],
        missingTime: [...a.missingTime, ...b.missingTime],
      }),
      initialGroupNR
    )
  );
};

export const queryToStatePotentials = (baseState: ReadonlyArray<IUserstateCollection>) => (
  config: IConfig
) => (queries: IQuery[]) => (
  query: IQuery,
  potentials: IPotentiality[],
  materials: IMaterial[]
): IRange[] => {
  if (!query.transforms) {
    return [configToRange(config)];
  }
  const transforms = query.transforms;
  const db = objectDBToDB(baseState);
  const timeTransfo = regroupTransfoTime(config, queries, potentials, materials);
  const [needSatis, needResources] = computeRangeSatisfaction(db, transforms, timeTransfo);
  const shrinkSpaces = computeShrinkSpace(potentials, materials);
  const configRange = configToRange(config);
  const outputSatis = computeOutputSatisfaction(
    configRange,
    rangeNeedSatisToDocs(needSatis),
    groupNeedResources(needResources),
    transforms,
    idToShrinkSpace(shrinkSpaces),
    '' + query.id
  );
  const outputRange = outputSatis.reduce(reduceOutputSatis, [configRange]);
  const result = intersect(simplify(needSatis.filter(rangeSatisEligible)), outputRange);
  if (result.length && result.every(range => range.start !== 0 || range.end !== 0)) {
    return result;
  }
  throw insatisToError(needSatis, outputSatis);
};

const reduceOutputSatis = (a: IRange[], b: ITransformSatisfaction): IRange[] => {
  const res = intersect(a, b.ranges);
  return res.length ? res : [{ start: 0, end: 0 }];
};

const rangeSatisToTransfoSatis = (
  rangeNeedSatis: IRangeNeedSatisfaction[]
): ITransformSatisfaction[] => {
  return unnest(
    rangeNeedSatis.map(nsObj =>
      nsObj.needSatisfactions
        .filter(ns => !ns.satisfied)
        .map(ns => ({ ranges: [{ start: nsObj.start, end: nsObj.end }], transform: ns.need }))
    )
  );
};

const insatisToError = (
  needSatis: IRangeNeedSatisfaction[],
  outputSatis: ITransformSatisfaction[]
) => {
  return [...rangeSatisToTransfoSatis(needSatis), ...outputSatis];
};

const rangeNeedSatiToDoc = (rangeNeed: IRangeNeedSatisfaction | undefined): IRefDoc[] =>
  rangeNeed
    ? rangeNeed.needSatisfactions.map(needSatis => ({
        collectionName: needSatis.need.collectionName,
        docs: needSatis.docs,
        ref: needSatis.need.ref,
      }))
    : [];
const rangeNeedSatisToDocs = (rangeNeeds: IRangeNeedSatisfaction[]) =>
  rangeNeedSatiToDoc(rangeNeeds.find(rangeNeed => rangeSatisEligible(rangeNeed)));

const areSameId = (a: IIdentifier, b: IIdentifier): boolean => {
  return Object.keys(a).every(k => a[k] === b[k]);
};

const idToShrinkSpace = (shrinkSpaces: Array<{ id: IIdentifier; space: number }>) => (
  id: IIdentifier
) => {
  const space = shrinkSpaces.find(sp => areSameId(sp.id, id)) as { id: IIdentifier; space: number };
  return space.space;
};

const potToId = (potential: IPotentiality, placeI: number): IIdentifier => ({
  place: `${placeI}`,
  potential: `${potential.potentialId}`,
  query: `${potential.queryId}`,
});

const potToShrinkSpace = (potential: IPotentiality) => (place: ReadonlyArray<IPotRange>, placeI: number) => {
  const id = potToId(potential, placeI);
  const range = placeToRange(place);
  const space = range.end - range.start - potential.duration.min;
  return {
    id,
    space,
  };
};

const matToId = (material: IMaterial): IIdentifier => ({
  material: `${material.materialId}`,
  query: `${material.queryId}`,
  split: `${material.splitId || 0}`,
});

const matToShrinkSpace = (material: IMaterial) => {
  const id = matToId(material);
  return {
    id,
    space: 0,
  };
};

const computeShrinkSpace = (potentials: IPotentiality[], materials: IMaterial[]) => {
  const potsShrink = unnest(
    potentials.map(potential => potential.places.map(potToShrinkSpace(potential)))
  );
  const matShrink = materials.map(matToShrinkSpace);
  return [...potsShrink, ...matShrink];
};

const regroupTransfoTime = (
  config: IConfig,
  queries: IQuery[],
  potentials: IPotentiality[],
  materials: IMaterial[]
) => {
  return groupWith<ITransformationTime>((a, b) => a.time === b.time)(
    sortByTime([
      ...configToTransforanges(config),
      ...mergePotsAndMatsToTransforanges(queries, potentials, materials),
    ])
  ).map(a => a.reduce(reduceTransfoGroup));
};
const reduceTransfoGroup = (a: ITransformationTime, b: ITransformationTime) => ({
  deletes: [...a.deletes, ...b.deletes],
  inserts: [...a.inserts, ...b.inserts],
  needs: [...a.needs, ...b.needs],
  time: a.time,
  updates: [...a.updates, ...b.updates],
});

const sortByTime = sortBy<ITransformationTime>(prop('time'));
const sortByMissingTime = (needResources: INeedResource[]) =>
  needResources.sort((a, b) => a.missingTime[0] - b.missingTime[0]);

const mergePotsAndMatsToTransforanges = (
  queries: IQuery[],
  potentials: IPotentiality[],
  materials: IMaterial[]
): ITransformationTime[] => {
  return sortByTime(
    unnest([
      ...potentials.map(potentialToTransforanges(queries)),
      ...materials.map(materialToTransforanges(queries)),
    ])
  );
};

const transfoToQueryTransfo = (id: IIdentifier) => <T extends allTransfo>(
  transfo: ReadonlyArray<T>
) => {
  return transfo.map(t => ({ id, transfo: t }));
};

/**
 * Use query chunk identifier. Compute shrink space before.
 */
const placeToTransfoTime = (
  id: IIdentifier,
  transfo: IQueryTransformationInternal,
  start: number,
  end: number
): ITransformationTime[] => {
  const toQueryTransfo = transfoToQueryTransfo(id);
  return [
    {
      deletes: [],
      inserts: [],
      needs: toQueryTransfo(transfo.needs),
      time: start,
      updates: [],
    },
    {
      deletes: toQueryTransfo(transfo.deletes),
      inserts: toQueryTransfo(transfo.inserts),
      needs: [],
      time: end,
      updates: toQueryTransfo(transfo.updates),
    },
  ];
};

const filterPlaceForPressure = (place: IPotRange) =>
  ['start', 'end', 'start-before', 'end-after'].includes(place.kind);

const placeToRange = (place: ReadonlyArray<IPotRange>): IRange => {
  const points = place
    .filter(filterPlaceForPressure)
    .map(c => {
      if (c.kind.startsWith('start')) {
        return c.start;
      }
      return c.end;
    })
    .sort((a, b) => a - b);
  return {
    end: points[1],
    start: points[0],
  };
};

const potentialToTransforanges = (queries: IQuery[]) => (
  potential: IPotentiality
): ITransformationTime[] => {
  const query: IQuery = queries.find(q => potential.queryId === q.id) as IQuery;
  const transform = query.transforms;
  if (!transform) {
    return [];
  }
  return unnest(
    potential.places.map((place, i) => {
      const range = placeToRange(place);
      return placeToTransfoTime(potToId(potential, i), transform, range.start, range.end);
    })
  );
};

const materialToTransforanges = (queries: IQuery[]) => (
  material: IMaterial
): ITransformationTime[] => {
  const query: IQuery = queries.find(q => material.queryId === q.id) as IQuery;
  const transform = query.transforms;
  if (!transform) {
    return [];
  }
  return placeToTransfoTime(matToId(material), transform, material.start, material.end);
};