Temat: Określenie wszystkich współdostępności zasobów. Może się...

Przy okazji realizacji pewnego projektu musiałem zmierzyć się z klasycznym problemem ustalenia czasów współdostępności zasobów.

Dana jest lista zasobów (urządzeń, osób, pomieszczeń), które są potrzebne do wykonania pewnego zadania. Każdy zasób ma na dany dzień swój grafik dostępności Od - Do. Trzeba znaleźć moment, kiedy wszystkie zasoby są dostępne. Takich momentów w ciągu doby może być oczywiście wiele. Zakładany najmniejszy czas dostępności zasobu to pół godziny, ale ja zrobiłem to dla minuty.

Po narysowaniu wykresu, gdzie na osi OX jest czas, a na OY - zasoby, rozwiązanie nasunęło się samo.
Można je także, po niewielkiej przeróbce, wykorzystać do detekcji "kolizji" terminów.

Ciekaw jestem też Waszych pomysłów. Ten jest bardzo prosty, ale też i elastyczny.

PS: Kod to proof-of-concept, więc nie czepiamy się bzdurek, polskich komentarzy i nie hejtujemy, że "to przedszkole" :)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Test
{
class FromTo
{
public DateTime From { get; set; }
public DateTime To { get; set; }
}

class Program
{
static void Main(string[] args)
{
/* ---------------------------------- Definiujemy nasze "zasoby" ---------------------------------- */

List<List<FromTo>> ResAvailability = new List<List<FromTo>>();

List<FromTo> resAvail1 = new List<FromTo>();
resAvail1.AddRange(new FromTo[]
{
new FromTo() { From = new DateTime(2013, 7, 24, 8, 0, 0), To = new DateTime(2013, 7, 24, 11, 0, 0) },
new FromTo() { From = new DateTime(2013, 7, 24, 13, 0, 0), To = new DateTime(2013, 7, 24, 15, 0, 0) },
new FromTo() { From = new DateTime(2013, 7, 24, 16, 0, 0), To = new DateTime(2013, 7, 24, 21, 0, 0) }
});

List<FromTo> resAvail2 = new List<FromTo>();
resAvail2.AddRange(new FromTo[]
{
new FromTo() { From = new DateTime(2013, 7, 24, 9, 0, 0), To = new DateTime(2013, 7, 24, 10, 0, 0) },
new FromTo() { From = new DateTime(2013, 7, 24, 12, 0, 0), To = new DateTime(2013, 7, 24, 16, 30, 0) }
});

List<FromTo> resAvail3 = new List<FromTo>();
resAvail3.AddRange(new FromTo[]
{
new FromTo() { From = new DateTime(2013, 7, 24, 10, 0, 0), To = new DateTime(2013, 7, 24, 22, 0, 0) }
});

ResAvailability.Add(resAvail1);
ResAvailability.Add(resAvail2);
ResAvailability.Add(resAvail3);

/* ---------------------------------- Umieszczamy dostępności w tablicy minutowych slotów ---------------------------------- */

const int MINUTES_IN_DAY = 24 * 60;

int[] slots = new int[MINUTES_IN_DAY]; // utwórz listę slotów o rozmiarze = liczbie minut w dobie

foreach (var resAvail in ResAvailability)
{
foreach (FromTo range in resAvail)
{
for (int i = DateToMins(range.From); i <= DateToMins(range.To); i++) // daty początku i końca dostępności na minuty
{
slots[i]++; // Inkrementuj slot o indeksie odpowiadającym dacie skonwertowanej do minut.
// Jeśli zakresy dat się nie nakładają (nie mają prawa), to dla każdego zasobu
// indeks ten będzie odwiedzony tylko raz.
}
}
}

/* ---------------------------------- Pobieramy "współdostępności" ---------------------------------- */

List<FromTo> result = new List<FromTo>();
int resCount = ResAvailability.Count;
bool inside = false;
int begin = 0;
int end = 0;

for (int i = 0; i < slots.Length; i++)
{
if (slots[i].Equals(resCount)) // jeśli w slocie jest tyle jedynek ile zasobów, to mamy komplet
{
if (!inside)
{
begin = i; // współdostępność zaczyna się tu...
inside = true;
}
}
else
{
if (inside)
{
inside = false;
end = i - 1; // ... a kończy na ostatniej pozycji spełniającej w/w kryterium

if (!begin.Equals(end)) // wykluczamy przypadek 10:00 - 10:00
{
result.Add(new FromTo() { From = MinsToDate(begin), To = MinsToDate(end) });
}
}
}
}

/* ---------------------------------- Przetwarzamy wedle potrzeb... ---------------------------------- */

foreach (FromTo range in result)
{
Console.WriteLine("{0} - {1}", range.From.ToString("HH:mm"), range.To.ToString("HH:mm"));
}
Console.Read();
}

static int DateToMins(DateTime date)
{
return date.Hour * 60 + date.Minute;
}

static DateTime MinsToDate(int min)
{
int hours = min / 60;
int mins = min % 60;
return new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, hours, mins, 0);
}
}
}


Wynik:
13:00 - 15:00
16:00 - 16:30
Ten post został edytowany przez Autora dnia 24.07.13 o godzinie 22:20
Marcin S.

Marcin S. Programista, trener
i konsultant w
zakresie .NET/.NET
Cor...

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Można to jeszcze łatwiej i ładniej rozwiązać za pomocą zbiorów z użyciem języka Linq.

Polecam.

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Marcin S.:
Można to jeszcze łatwiej i ładniej rozwiązać za pomocą zbiorów z użyciem języka Linq.

Polecam.

O, fajnie :) A masz może jakieś przykładowe rozwiązanie?
Dane wejściowe: kolekcja zasobów, z których każdy posiada zbiór swoich dostępności w ciągu dnia.
Marcin S.

Marcin S. Programista, trener
i konsultant w
zakresie .NET/.NET
Cor...

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Niestety nie mam teraz czasu aby szukać odpowiedniego kodu.

Poczytaj sobie o Linq'u i o operacjach na zbiorach (cześć wspólna itd).
Składnia Linq jest podobna do SQL'a, więc jeśli znasz SQL'a nie powinieneś mieć problemów z Linq.
Ale nawet jeśli nie znasz, to też powinieneś sobie poradzić.

A propos twojego kodu - on jest mało czytelny.
Na przykład można dużo łatwiej wprowadzać dane testowe za pomocą inicjalizatorów, zamiast stosowanie metody AddRange:


var resAvail1 = new List<FromTo>
{

new FromTo() { From = new DateTime(2013, 7, 24, 8, 0, 0), To = new DateTime(2013, 7, 24, 11, 0, 0) },
new FromTo() { From = new DateTime(2013, 7, 24, 13, 0, 0), To = new DateTime(2013, 7, 24, 15, 0, 0) },
new FromTo() { From = new DateTime(2013, 7, 24, 16, 0, 0), To = new DateTime(2013, 7, 24, 21, 0, 0) }
});



Wpisane z palca, nie testowe, więc może wymagać kosmetycznych poprawek. Chodziło mi o pokazanie samej idei.

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Marcin S.:
Niestety nie mam teraz czasu aby szukać odpowiedniego kodu.

Poczytaj sobie o Linq'u i o operacjach na zbiorach (cześć wspólna itd).

Znam LINQ. Prosiłem o przykład, ponieważ interesuje mnie sam algorytm i jego złożoność obliczeniowa, a także prostota zapisu.

To, co robi mój kod, to praktycznie przecięcie wielu zbiorów na raz i robi to szybko. Jest jedna tablica "operacyjna" o długości=liczba_minut_w_dobie, bezpośrednio indeksowana czasem skonwertowanym do liczby minut (konwersja odbywa się tylko raz, w definicji warunków pętli), więc czas dostępu do dowolnego jej elementu jest stały (1). Zakresy od-do nie muszą być sortowane.
Odwołań do tablicy jest c*n + C, gdzie c to liczba elementów do odwiedzenia, n to liczba wszystkich dostępności (liczba_zasobow * liczba_dostepnosci_zasobu), zaś C to liczba wszystkich elementów tablicy (minut w dobie), ponieważ na końcu trzeba ją przejrzeć i odczytać z niej początki i końce zakresów.

Wielkość c zależy liczby zasobów i długości ich dostępności. Skrajnie, gdy każdy zasób jest dostępny full time, mam przejście całej tablicy (c=C) i złożoność to O(C*(n+1)).

To się raczej rzadko zdarzy, a ponadto można wykrywać, że wszystkie zasoby mają po 1 dostępności i wtedy użyć prostego algorytmu biorącego listę dostępności i obliczającego jedynie max(czas_początku) - min(czas_końca).

Przeciętnie, dla 8 godzin jest to C/3. Jeśli w tym czasie są jeszcze przerwy/przestoje, to c dalej się zmniejsza. Co prawda rośnie n (wzrasta liczba dostępności per zasób), ale i tak c*n<C*liczba_zasobow.

Dodatkowo robi to prosto. Deklarowana jest tablica "operacyjna". Następuje iteracja pozasobach i ich dostępnościach, w trakcie której są zaznaczane "odwiedziny" w odpowiednich komórkach (indeks tabeli = minuta). Dla każdego zasobu dana komórka zostanie odwiedzona jedynie raz.

Potem w jednym przebiegu sprawdzamy, czy liczba "odwiedzin" = liczba zasobów i odczytujemy początki i końce zakresów współdostępności (już posortowane).

> A propos twojego kodu - on jest mało czytelny.
Na przykład można dużo łatwiej wprowadzać dane testowe za pomocą inicjalizatorów, zamiast stosowanie metody AddRange:

Wezmę to pod uwagę, dziękuję. Choć ostatecznie to zupełnie podstawowa konstrukcja dodania elementów do tablicy i wydaje mi się, że nie trzeba siedzieć godzinami skrobiąc się po głowie i myśląc "co on tutaj robi?!" :) Mam nadzieję, że reszta kodu jest już czytelna, bo użyłeś kwantyfikatora ogólnego :)Ten post został edytowany przez Autora dnia 25.07.13 o godzinie 11:37

konto usunięte

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Jedyny komentarz który tu pasuje to zrefactorowany kod z użyciem linq... inaczej,
nie wiem po co te uwagi, skoro autor napisał w PS, że to proof of concept...Ten post został edytowany przez Autora dnia 25.07.13 o godzinie 13:40
Marcin S.

Marcin S. Programista, trener
i konsultant w
zakresie .NET/.NET
Cor...

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Skoro znasz Linq użyj go! Wydaje mi się, że można to zrobić w jednym zapytaniu. Jak znajdę czas to wrzucę rozwiązanie.

No niestety, moja uwaga co do czytelności kodu dotyczyła całego kodu (w tym algorytm). To klasyczny przykład "spaghetti code" :) Ale spoko, do pięknych form dochodzi się latami :)

Przy okazji, dlaczego używasz Equals zamiast znaku równości? W tym przypadku zwykłe porównanie wg mnie wystarczy.
Marcin S.

Marcin S. Programista, trener
i konsultant w
zakresie .NET/.NET
Cor...

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Grzegorz G.:
Jedyny pasujący komentarz który tu pasuje to zrefactorowany kod z użyciem linq... inaczej,
nie wiem po co te uwagi, skoro autor napisał w PS, że to proof of concept...

Zdaje sobie sprawę, że to "proof of concept", ale chciałem od razu wygładzić kod :)
To nie żadne uwagi i mam nadzieję, że Adrian wyczuwa, ze mam dobre intencje.

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Marcin S.:
Skoro znasz Linq użyj go! Wydaje mi się, że można to zrobić w jednym zapytaniu. Jak znajdę czas to wrzucę rozwiązanie.

Dzięki :) Ale to nic pilnego, raczej luźna dyskusja.
No niestety, moja uwaga co do czytelności kodu dotyczyła całego kodu (w tym algorytm). To klasyczny przykład "spaghetti code" :) Ale spoko, do pięknych form dochodzi się latami :)

Kilka ładnych lat temu pewien człowiek, niezły spec od algorytmów (obecnie zasila już zagraniczne budżety) powiedział mi tak: "kiedy robisz PC, nie baw się w refaktoryzowanie kodu. Jeśli chcesz komuś pokazać zasadę, wrzucaj wszystko - od main, przez inicjalizację, algorytm, aż po return. Refaktoryzacja jest piękna, ale z doświadczenia algorytmika powiem ci, że niejeden problem na etapie "rozkminy" powstał właśnie przez to. Bierz kartkę, długopis i pisz tak, byś zrozumiał. Nie bez powodu autorzy podręczników algorytmiki i metod numerycznych stosują takie właśnie podejście. A potem dziel na procedury, skracaj kod i cyzeluj. Pamiętasz - kod funkcji ma się mieścić w 25 liniach :)".

Z powodzeniem stosuję od lat podczas dyskusji. Zasady "czystego kodu", wypracowywane przez ponad 15 lat programowania, zachowuję na potrzeby produkcji. Ale to chyba nie temat o "code smells", o co prosiłem w pierwszym poście tego wątku.

Przy okazji, dlaczego używasz Equals zamiast znaku równości? W tym przypadku zwykłe porównanie wg mnie wystarczy.

Nawyk z czasów C, gdzie czasem zapominało się, szybko pisząc, o ==.Dziś kompilator o tym ostrzega, ale nawyk pozostał. Zresztą tak samo porównuję wszystkie typy. Lubię spójność.
Marcin S.:
Grzegorz G.:
Jedyny pasujący komentarz który tu pasuje to zrefactorowany kod z użyciem linq... inaczej,
nie wiem po co te uwagi, skoro autor napisał w PS, że to proof of concept...

Zdaje sobie sprawę, że to "proof of concept", ale chciałem od razu wygładzić kod :)
To nie żadne uwagi i mam nadzieję, że Adrian wyczuwa, ze mam dobre intencje.

Ależ oczywiście :) I bardzo dobrze, że jesteś uczulny na code smells. Sam prowadzę sporo dyskusji na ten temat ze stażystami. Tutaj jednak chodziło mi o dyskusję raczej algorytmiczną.Ten post został edytowany przez Autora dnia 25.07.13 o godzinie 12:02

Temat: Określenie wszystkich współdostępności zasobów. Może się...

OK, faktycznie, jedno zapytanie.

var res = ResAvailability.Select(p => 
p.Select(q =>
Enumerable.Range(DateToMins(q.From), (DateToMins(q.To) - DateToMins(q.From)))
).Aggregate((x, y) => x.Concat(y))
).Aggregate((x,y) => x.Intersect(y))
.ToArray();


Przeskanowanie tablicy wynikowej w poszukiwaniu nieciągłości:
List<DateTime> from = new List<DateTime>();
List<DateTime> to = new List<DateTime>();

from.Add(MinsToDate(res[0]));
for (int i = 0; i < res.Count()-1; i++)
{
if (res[i + 1] - res[i] > 1)
{
to.Add(MinsToDate(res[i]).AddMinutes(1));
from.Add(MinsToDate(res[i+1]));
}
}
to.Add(MinsToDate(res[res.Count()-1]).AddMinutes(1));


OK, dzięki za naprowadzenie na dobrą drogę :)Ten post został edytowany przez Autora dnia 25.07.13 o godzinie 13:00
Marcin S.

Marcin S. Programista, trener
i konsultant w
zakresie .NET/.NET
Cor...

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Dlaczego konwertujesz czas do minut?
Nie wystarczy operowanie bezpośrednio na datach (DateTime) lub czasie (TimeSpan)?

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Z uwagi na możliwość bezpośredniego indeksowanie tablicy w pierwszym algorytmie, a w drugim - wygenerowania sekwencji liczb. Ten drugi sposób co prawda działa inaczej, ale w obu przypadkach operuję na wektorach od-do.


Obrazek


-----------------------

Swoją drogą - ostrożnie z nazywaniem tego "refaktoryzacją"! To już jest inny algorytm! Refaktoryzacja nie zmienia postaci algorymu.

U mnie nie generowały się żadne sekwencje liczb dla zasobów, a jedynie granice pętli for. Była jedna tablica slotów, po której szły przebiegi inkrementujące wartości odpowiednich komórek.

Nie wiem, jak w LINQ wygląda algorytmicznie przecięcie zbiorów, ale jeśli zastosowali hashmapę (moja tablica to taki bardzo prosty jej przykład, gdzie funkcja skrótu danych (DateTime) oblicza po prostu jego minutową reprezentację), to utworzyła się dodatkowa struktura - jak u mnie.

Być może pod względem złożoności wyszło podobnie, a być może nie, trzeba by sprawdzić. Tutaj to jest pierdółka nie warta zastanawiania, ale są przypadki, gdzie kod zostałby wycofany, o ile nie jest jedynie "innym zapisem przyjętego algorytmu".Ten post został edytowany przez Autora dnia 25.07.13 o godzinie 13:32
Marcin S.

Marcin S. Programista, trener
i konsultant w
zakresie .NET/.NET
Cor...

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Wydaje mi się, że bardziej optymalne będzie operowanie na zakresach dat (od do) niż sekwencji liczb.
Generowanie sekwencji nie jest wg mnie potrzebne.

Masz zasoby i przedziały czasowe, które tworzą zbiory (zakresy).
A ty szukasz iloczynu wszystkich zbiorów, aby otrzymać zbiór wynikowy (podzbiór będący częścią wspólną) prawda?

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Dokładnie tak. Tam wyżej masz ilustrację zrobioną na szybko w Excelu.

Uwaga: takich części wspólnych w ciągu dnia może być wiele - jak na ilustracji.Ten post został edytowany przez Autora dnia 25.07.13 o godzinie 13:45
Marcin S.

Marcin S. Programista, trener
i konsultant w
zakresie .NET/.NET
Cor...

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Rozumiem, czyli w wyniku możemy otrzymać kilka zbiorów z zakresami.

Pomyślę jak to zrobić przy pomocy samego Linq bo podoba mi się problem :)

Temat: Określenie wszystkich współdostępności zasobów. Może się...

O to to :)
Dokładnie tak.

/ Ja chyba jestem już dinozaurem, ale w algorytmice jakoś bardziej "przemawia" do mnie "łopatologiczne rozpisanie na forach", gdzie dokładnie widzę co się dzieje w środku i jakie struktury powstają. Co nie zmienia faktu, że uważam LINQ za genialne ułatwienie w codziennej deweloperce, zwłaszcza przy okazji pisania niezależnej od ORM/DBMS warstwy dostępu do danych /
Marcin S.

Marcin S. Programista, trener
i konsultant w
zakresie .NET/.NET
Cor...

Marcin S.

Marcin S. Programista, trener
i konsultant w
zakresie .NET/.NET
Cor...

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Adrian O.:
O to to :)
Dokładnie tak.

/ Ja chyba jestem już dinozaurem, ale w algorytmice jakoś bardziej "przemawia" do mnie "łopatologiczne rozpisanie na forach", gdzie dokładnie widzę co się dzieje w środku i jakie struktury powstają. Co nie zmienia faktu, że uważam LINQ za genialne ułatwienie w codziennej deweloperce, zwłaszcza przy okazji pisania niezależnej od ORM/DBMS warstwy dostępu do danych /

Mam identyczny staż jak twój i też wychowałem się na pętlach for :)
Ale wiele problemów łatwiej jest rozwiązać operując na zbiorach.
Dlatego Linq jest tak genialny. A tak naprawdę to nie sam język jest genialny, lecz właśnie samo podejście.

Ciekawe kiedy wprowadzą do C# natywne operacje na grafach ;-)Ten post został edytowany przez Autora dnia 25.07.13 o godzinie 14:04

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Wiem, sporo takich artykułów czytałem wczoraj. Mnie jednak nie interesuje kolizja terminów (A zachodzi na B), tylko znalezienie zakresów wszystkich części wspólnych (początek i koniec A1/\B1/\...../\X1, początek i koniec A2/\B2/\...../\X2, etc.).

Oczywiście z tego drugiego zagadnienia niemal natychmiast dostaje się "test kolizji", ale nie odwrotnie.

PS: plus za TimePeriod na CP :) Niestety, nie mogę zastosować "third-party" bibliotek poza ustalony już zbiór i mam starać się określać złożoności tego, co implementuję.Ten post został edytowany przez Autora dnia 25.07.13 o godzinie 14:14
Marcin S.

Marcin S. Programista, trener
i konsultant w
zakresie .NET/.NET
Cor...

Temat: Określenie wszystkich współdostępności zasobów. Może się...

Adrian O.:
Wiem, sporo takich artykułów czytałem wczoraj. Mnie jednak nie interesuje kolizja terminów (A zachodzi na B), tylko znalezienie zakresów wszystkich części wspólnych (początek i koniec A1/\B1/\...../\X1, początek i koniec A2/\B2/\...../\X2, etc.).

Pomyślałem, że coś można z tego wyciągnąć.
PS: plus za TimePeriod na CP :) Niestety, nie mogę zastosować "third-party" bibliotek poza ustalony już zbiór i mam starać się określać złożoności tego, co implementuję.

OK, rozumiem.



Wyślij zaproszenie do