Codeminer42/cm42-central

View on GitHub
app/assets/javascripts/models/beta/iteration.js

Summary

Maintainability
B
4 hrs
Test Coverage
import moment from 'moment';
import { last, isEmpty } from 'underscore';
import * as Story from './story';

const weeksBetween = (dateA, dateB) =>
  moment(new Date(dateA)).diff(moment(new Date(dateB)), 'week');

const getIterationForDate = (date, project) => {
  const weeks = weeksBetween(date, project.startDate);
  const iterationQuantity = Math.ceil(weeks / project.iterationLength);

  return project.iterationLength > weeks
    ? iterationQuantity
    : iterationQuantity + 1;
};

const getNextIterationNumber = iterations => {
  const lastIteration = last(iterations);

  return lastIteration ? lastIteration.number + 1 : 1;
};

const getStartDate = (iterationNumber, project) => {
  const projectStartDate = moment(project.startDate, 'YYYY/MM/DD');

  if (iterationNumber === 1) {
    return projectStartDate;
  }

  const missingDaysFromFirsIteration = firstIterationMissingDays(project);
  const daysUntilIteration = daysUntil(iterationNumber, project);
  const expectedProjectStartDate = projectStartDate.subtract(
    missingDaysFromFirsIteration,
    'days'
  );

  return expectedProjectStartDate.add(daysUntilIteration, 'days');
};

const firstIterationMissingDays = project =>
  (moment(project.startDate, 'YYYY/MM/DD').isoWeekday() -
    project.iterationStartDay) %
  7;

const daysUntil = (iterationNumber, project) =>
  (iterationNumber - 1) * (project.iterationLength * 7);

export const getDateForIterationNumber = (iterationNumber, project) =>
  getStartDate(iterationNumber, project).format('YYYY/MM/DD');

export const getCurrentIteration = project =>
  getIterationForDate(new Date(), project);

export const getIterationForStory = (story, project) => {
  if (story.state === 'accepted') {
    return getIterationForDate(new Date(story.acceptedAt), project);
  }
};

const createSprint = (
  sprintNumber = 0,
  startDate = 0,
  isFiller = false,
  velocity,
  stories = [],
  points = 0,
  remainingPoints = calculateRemainingPoints(velocity, points)
) => ({
  number: sprintNumber,
  startDate,
  points,
  completedPoints: null,
  stories,
  isFiller,
  remainingPoints,
  isDropDisabled: false,
});

export const calculateRemainingPoints = (velocity, points) =>
  velocity - points > 0 ? velocity - points : 0;

const createFillerSprints = (
  quantityFillers,
  initialNumber,
  project,
  velocity,
  storyPoints
) =>
  _.times(quantityFillers, i => {
    const remainingPoints = isLastFiller(i, quantityFillers)
      ? calcPointsOverflowedSprint(velocity, storyPoints)
      : 0;
    const sprintNumber = initialNumber + i;
    const emptyStories = [];
    const sprintPoints = 0;
    return createSprint(
      sprintNumber,
      getDateForIterationNumber(sprintNumber, project),
      true,
      velocity,
      emptyStories,
      sprintPoints,
      remainingPoints
    );
  });

const createOverflowedSprint = (
  velocity,
  storyPoints,
  fillersSprints,
  project,
  overflowStory
) => {
  const numberSprint = getNextIterationNumber(fillersSprints);

  return createSprint(
    numberSprint,
    getDateForIterationNumber(numberSprint, project),
    true,
    velocity,
    [overflowStory],
    storyPoints,
    calcPointsOverflowedSprint(velocity, storyPoints)
  );
};

export const canTakeStory = (sprint, story) =>
  sprint.remainingPoints >= Story.getPoints(story) || !Story.isFeature(story);

export const calculateFillerSprintsQuantity = (storyPoints, velocity) =>
  Math.ceil((storyPoints - velocity) / velocity) || 0;

const addSprintWithOverflow = (
  project,
  sprints,
  story,
  velocity,
  firstSprintNumber
) => {
  const currentSprint = last(sprints);
  const storyPoints = Story.getPoints(story);
  const quantityFillers = calculateFillerSprintsQuantity(storyPoints, velocity);

  if (
    isFirstSprint(currentSprint, firstSprintNumber) &&
    haveNoPoints(currentSprint)
  ) {
    addStoryToSprint(project, sprints, 0, story, velocity);
    return sprints;
  } else {
    const indexOverflow = sprintIndexFor(currentSprint);
    const fillerSprints = createFillerSprints(
      quantityFillers,
      indexOverflow,
      project,
      velocity,
      storyPoints
    );
    const overflowedSprint = createOverflowedSprint(
      velocity,
      storyPoints,
      fillerSprints,
      project,
      story
    );
    return [...sprints, ...fillerSprints, overflowedSprint];
  }
};

const sprintIndexFor = sprint => sprint.number + 1;

const addStoryToSprint = (project, sprints, index, story, velocity) => {
  sprints[index].stories.push(story);
  sprints[index].points += Story.getPoints(story);
  const lastValidSprintIndex = sprints.length - 2;

  let previousFillerSprintsQuantity = 0;
  for (let i = lastValidSprintIndex; i >= 0; i--) {
    if (sprints[i].isFiller) {
      previousFillerSprintsQuantity++;
    } else {
      break;
    }
  }
  fillRemainingPoints(sprints, Story.getPoints(story), index);

  sprints[index].completedPoints += Story.getCompletedPoints(story);

  return sprints;
};

const fillRemainingPoints = (sprints, storyPoints, index) =>
  (sprints[index].remainingPoints -= storyPoints);

const createFirstSprint = (project, initialSprintNumber, velocity) =>
  createSprint(
    initialSprintNumber,
    getDateForIterationNumber(initialSprintNumber, project),
    false,
    velocity
  );

export const sprintVelocity = (project, pastIterations) => {
  return pastIterations.length < 3
    ? project.defaultVelocity
    : pastAverageVelocity(pastIterations);
};

const pastAverageVelocity = pastIterations => {
  const points = pastIterations.slice(-3);
  const result =
    points.reduce((sum, iteration) => sum + iteration.points, 0) / 3;
  return Math.floor(result) || 1;
};

const addToSprintFromBacklog = (sprints, project, story, velocity) => {
  const iterationNumber = getNextIterationNumber(sprints);

  return [
    ...sprints,
    createSprint(
      iterationNumber,
      getDateForIterationNumber(iterationNumber, project),
      false,
      velocity,
      [story],
      Story.getPoints(story)
    ),
  ];
};

export const groupBySprints = (
  stories = [],
  project,
  initialSprintNumber = 1
) => {
  const velocity = project.currentSprintVelocity;

  return stories.reduce((sprints, story) => {
    const firstSprintIndex = 0;
    const isFromSprintInProgress = Story.isInProgress(story);

    if (isEmpty(sprints)) {
      sprints = [createFirstSprint(project, initialSprintNumber, velocity)];
    }

    if (causesOverflow(story, velocity) && !isFromSprintInProgress) {
      return addSprintWithOverflow(
        project,
        sprints,
        story,
        velocity,
        initialSprintNumber
      );
    }

    if (isFromSprintInProgress) {
      return addStoryToSprint(
        project,
        sprints,
        firstSprintIndex,
        story,
        velocity
      );
    }

    if (canEnterLastAddedSprint(sprints, story)) {
      return addStoryToSprint(
        project,
        sprints,
        sprints.length - 1,
        story,
        velocity
      );
    }

    return addToSprintFromBacklog(sprints, project, story, velocity);
  }, []);
};

const isFirstSprint = (sprint, initialSprintNumber) =>
  sprint.number === initialSprintNumber;

const haveNoPoints = sprint => sprint.points === 0;

const canEnterLastAddedSprint = (sprints, story) => {
  const lastSprint = last(sprints);

  return canTakeStory(lastSprint, story);
};

export const causesOverflow = (story, velocity) =>
  Story.getPoints(story) > velocity && Story.isFeature(story);

const calcPointsOverflowedSprint = (velocity, storyPoints) =>
  velocity - (storyPoints % velocity);

const isLastFiller = (position, amountOfFillers) =>
  position === amountOfFillers - 1;

export const isNewStoryPosition = (sprint, position) =>
  !Boolean(sprint.stories[position]);