рассчитать общую продолжительность диаграммы Ганта

Я создаю диаграмму, подобную диаграмме Ганта (на самом деле конфигурация), и мне нужно рассчитать общую продолжительность и проверить настраиваемые длительности. Цель состоит в том, чтобы пользователи могли построить диаграмму Ганта, не зная дат, но зная задачи и то, как они (в общих чертах) связаны. На предыдущем шаге пользователи добавляют задачи и выбирают начальные и конечные шаги для этих задач. Шаги имеют фиксированный порядок. Фактические даты неизвестны (или актуальны), но будут сопоставлены с шагами позже.

Большинство инструментов Ганта, которые я видел, полагаются на знание дат начала и окончания и не выполняют вычислений.

Как мне рассчитать общую продолжительность, а также проверить, если продолжительность недействительна? Очевидно, что в некоторых случаях невозможно подсчитать общую сумму: если между действиями есть неиспользованный шаг. Простая недопустимая продолжительность будет иметь место, если две задачи имеют одинаковую дату начала и окончания, но имеют разные значения. Более сложный может возникнуть, когда 2 или более действий имеют разные шаги начала / конца и перекрываются.

Я ищу не полное решение (в любом случае, с моим текущим кодом оно вряд ли пригодится), а скорее общий алгоритм или подход. Я бы подумал, что рекурсивное решение имеет смысл, но поскольку я делаю это с помощью JS / jQuery / DOM, меня беспокоит производительность рекурсивного решения, которое должно многократно искать элементы. С чего начать, с конца или с начала? следует ли мне следить за началом / концом каждого шага, пока я не перестану двигаться дальше, или переоценить, какой шаг добавить к общей продолжительности в середине?

Вот изображение текущей разметки: screenshot


person JoeBrockhaus    schedule 27.12.2013    source источник
comment
Насколько я понимаю, вы в основном просите нас сделать весь ваш проект за вас. Или хотя бы как это сделать. Вместо этого проведите небольшое исследование. Как рассчитать продолжительность? Что ж, есть МНОГО способов сделать это в зависимости от ваших конкретных потребностей. Я советую изучить их и задать более конкретный вопрос.   -  person cjds    schedule 27.12.2013
comment
en.wikipedia.org/wiki/Program_Evaluation_and_Review_Technique?   -  person Guy Sirton    schedule 27.12.2013


Ответы (1)


Попробую объяснить, чем я закончил.

Я думаю, чтобы следовать, вам нужно немного узнать о требованиях. Этот интерактивный / настраиваемый график / график используется в качестве шаблона для оценки сроков производства.

Всего 3 штуки:

  • Шаги
  • мероприятия
  • Продолжительность действий, которая различается в зависимости от типа объекта, к которому применяется расписание.

Поскольку это шаблон, используемый для оценки, изначально нет дат - только произвольная длительность, привязанная к действиям, сопоставленным с шагами. Однако в конечном итоге шаги сопоставляются с датами из импортированного отчета (или вводятся вручную).

Есть 3 страницы, на которых пользователь постепенно составляет расписание:

  • Добавить / изменить шаги: шаги - это строки, которые создаются со значением порядка сортировки (предполагаемым).
  • Добавление / редактирование действий: матрица с шагами в виде столбцов и действиями в виде строк. Каждый перекресток - это флажок. Для каждого действия необходимо выбрать начальный и конечный этапы.
  • Добавить / изменить продолжительность: выбирается тип элемента, и продолжительность добавляется для каждого действия.

Классы

  • Шаг [Имя, StepOrder, ..]
  • Действие [Имя, StartStepID, StartStepOrder, EndStepID, EndStepOrder, ..]
  • ActivityDuration: Activity [Duration, ItemType, ..]

В контроллере / репозитории MVC:

        // start by ordering the durations by the order of the steps
        var sortedActivities = durations
            .OrderBy(x => x.StartStepOrder)
            .ThenBy(x => x.EndStepOrder);

        // create func to get the path name from the old + new activity
        var getActivityPath = new Func<string, string, string>(
            (prevPath, activityID) =>
            {
                return string.IsNullOrEmpty(prevPath)
                    ? string.Format("{0}", activityID)
                    : string.Format("{0}.{1}", prevPath, activityID);
            });

        // create the recursive func we'll call to do all the work
        Action<List<ActivityDuration>, string, long?, IEnumerable<ActivityDuration>> buildPaths = null;
        buildPaths = (activities, path, startStepID, starts) =>
        {
            // activities will be null until we are joining gapped paths, 
            // so grab the activities with the provided startStepID
            if (starts == null)
                starts = activities.Where(x => x.StartStepID == startStepID);

            // each activity represents a new branch in the path
            foreach (var activity in starts)
            {
                var newPath = getActivityPath(path, activity.Id.ToString());

                // add the new path and it's ordered collection 
                // of activities to the collection
                if (string.IsNullOrEmpty(path))
                {
                    paths.Add(newPath, new ActivityDuration[] { activity });
                }
                else
                {
                    paths.Add(newPath, paths[path].Concat(new ActivityDuration[] { activity }));
                }

                // do this recursively providing the EndStepID as the new Start
                buildPaths(activities, newPath, activity.EndStepID, null);
            }

            // if there were any new branches, remove the previous 
            // path from the collection
            if (starts.Any() && !string.IsNullOrEmpty(path))
            {
                paths.Remove(path);
            }
        };


        // since the activities are in step order, the first activity's 
        // StartStepID will be where all paths start.
        var firstStepID = sortedActivities.FirstOrDefault().StartStepID;

        // call the recursive function starting with the first step
        buildPaths(sortedActivities.ToList(), null, firstStepID, null);

        // handle gaps in the paths after all the first connected ones have been pathed.
        //   :: ie - step 1,2 & 4,5 are mapped, but not step 3
        // these would be appended to the longest path with a step order < start step of the gapped activity's start step (!!!) 
        //   :: ie - the path should be 1-2,2-4,4-5)

        // because the list of paths can grow, we need to keep track of the count
        // and loop until there are no more paths added
        var beforeCount = paths.Count;
        var afterCount = beforeCount + 1;
        while (beforeCount < afterCount)
        {
            foreach (var path in paths.ToArray())
            {
                var lastActivity = path.Value.Last();
                // check for activities that start after the last one in each path .. 
                // .. that don't start on another activity's end step (because that would be a part of a path already)
                var gapped = sortedActivities
                    .Where(x => x.StartStepOrder > lastActivity.EndStepOrder)
                    .Where(thisAct =>
                        !sortedActivities
                            .Select(otherAct => otherAct.EndStepID)
                            .Contains(thisAct.StartStepID)
                    );

                beforeCount = paths.Count;
                // for each new gapped path, build paths as if it was specified by the previous activity
                buildPaths(sortedActivities.ToList(), path.Key, null, gapped);
                afterCount = paths.Count;
            }
        }

        // build up an object that can be returned to 
        // JS with summations and ordering already done.
        rValue = new ActivityPaths()
        {
            Paths = paths
                .Select(x => new ActivityPath()
                {
                    Path = x.Key,
                    ActivityDurations = x.Value,
                    TotalDuration = x.Value.Sum(y => y.Duration)
                })
                .OrderByDescending(x => x.TotalDuration)
        };

По общему признанию, у этой конструкции есть некоторые недостатки, но сценарии использования это позволяют. В частности: - Действие не может напрямую иметь более одного зависимого шага - или, другими словами, 2 шага не могут иметь одинаковый порядок шагов. - Если 2 пути имеют одинаковую общую продолжительность, только один будет отображаться как критический.

Так как даты, которые сопоставлены с шагами, в конечном итоге используются для вычисления назад / вперед до конца пути с заданного момента времени, это нормально. После того, как все даты указаны, при необходимости можно рассчитать более точный критический путь.

Возвращается весь набор путей, так что некоторая проверка может быть реализована в javascript. Первый путь будет критическим "единичным", и этот путь будет выделен в пользовательском интерфейсе с отображением общей продолжительности критического пути:  сетка продолжительности с критическим путем

person JoeBrockhaus    schedule 22.01.2014