Lab04

Laboratorium 4: BCL, Kolekcje i Wyrażenia Lambda #

Przetwarzanie tekstu, parsowanie #

Co to jest REST API?

REST (Representational State Transfer) to architektoniczny styl projektowania usług sieciowych, który wykorzystuje standardowe operacje HTTP (GET, POST, PUT, DELETE) do komunikacji klient‑serwer.

  • Zasoby identyfikowane są przy pomocy URL (np. https://api/users/123).
  • Metody HTTP określają akcję (np. pobranie, utworzenie, modyfikację).
  • Każdy zapytanie zawiera wszystkie informacje potrzebne do jego przetworzenia (bezstanowość ang. stateless).

W praktyce niemal każda nowoczesna aplikacja webowa czy mobilna udostępnia REST API.

Ścieżki do zasobów (ang. resource paths)

Ścieżki w URL określają, do jakich zasobów i w jakiej hierarchii chcemy się odwołać na serwerze. Każdy segment po hoście to albo nazwa zasobu, albo jego identyfikator.

  • /v1/users/42: wersja 1, zasób users, identyfikator 42.
  • /v2/countries/100/cities/3: wersja 2, zasób countries (id 100), a następnie zagnieżdżony zasób cities (id 3).

Parametry zapytania (ang. query parameters)

Parametry zapytania, umieszczone po znaku ?, pozwalają przekazywać dodatkowe dane do serwera:

  • Każdy parametr to klucz=wartość (np. lang=pl).
  • Poszczególne pary klucz=wartość oddzielane są znakiem &, np. ?tag=new&active=true.

Opis zadania #

Twoim zadaniem jest zaimplementowanie metody:

public static (ParsedUrl url, ParsingStatus status) ParseUrl(string url);

Metoda powinna:

  • Przyjmować jako parametr ciąg znaków url.
  • Zwracać krotkę o dwóch nazwanych elementach:
    • url - obiekt klasy ParsedUrl zawierający szczegóły parsowanego adresu.
    • status - wartość typu wyliczeniowego ParsingStatus informującą o powodzeniu lub przyczynie błędu parsowania.
  • W przypadku nieudanego parsowania, zawartość obiektu ParsedUrl jest nieistotna (kluczowy jest wówczas status).

Definicje typów pomocniczych:

// Schemat protokołu
public enum UrlScheme
{
	Http,
	Https,
	Ftp,
	Wss
}

// Segment ścieżki (nazwa zasobu + identyfikator)
public sealed class ResourceSegment
{
	public string Name { get; set; } = string.Empty;
	public int Id { get; set; }
}

// Status parsowania (informuje o powodzeniu lub błędzie)
public enum ParsingStatus
{
	UnexpectedFormat,
	Success,
	InvalidScheme,
	InvalidHost,
	InvalidVersion,
	InvalidPath,
	InvalidId,
	InvalidQuery,
}

// Typ zwracany przez metodę
public sealed class ParsedUrl
{
	public UrlScheme Scheme { get; set; }
	public string Host { get; set; } = string.Empty;
	public int Version { get; set; }
	public List<ResourceSegment> PathSegments { get; set; } = [];
	public Dictionary<string, List<string>> QueryParams { get; set; } = [];
}

Struktura oczekiwanego URL

[scheme]://[host]/v[version]/[resource1]/[id1]/[resource2]/[id2]/...?[param1]=[valueA]&[param1]=[valueB]&[param2]=[valueC]...

gdzie:

  • scheme: jeden z elementów typu wyliczeniowego UrlScheme,
  • host: nazwa domeny (np. example.com),
  • version: liczba całkowita (prefiks v, np. v2),
  • resourceN: nazwa zasobu (ciąg znaków bez /),
  • idN: całkowitoliczbowy identyfikator zasobu,
  • paramN: nazwa parametru,
  • valueN: wartość parametru.

Uwagi implementacyjne

  • Parametry zapytania:
    • W zadaniu przyjmujemy, że nazwa parametru może się pojawić na liście parametrów wielokrotnie.
    • Wszystkie wartości dla danego parametru agregowane są w postaci listy.
  • Operacje na klasie string:
    • Nie korzystaj z System.Uri.
    • Używaj gotowych metod klasy string (np. Split, IndexOf, Substring, Contains).
  • Status parsowania:
    • Success: parsowanie bez błędów,
    • InvalidScheme: niezgodny lub nieobsługiwany scheme,
    • InvalidHost: brak części odpowiadającej za hosta,
    • InvalidVersion: wersja nie parsuje się na int lub jest < 1,
    • InvalidPath: błędna liczba segmentów (np. brak identyfikatora dla ostatniego zasobu),
    • InvalidId: identyfikator jest niepoprawny (zasady te same jak dla wersji),
    • InvalidQuery: błędny format query (np. brak =),
    • UnexpectedFormat: dowolny inny błąd.
  • Debuggowanie: Zadanie jest dobrą okazją do zaznajomienia się z działaniem debuggera.

Materiały pomocnicze:

Przykłady

Poprawny URL z jednym segmentem i jednym parametrem
var input1 = "http://example.com/v1/users/42?lang=pl";
var (parsed1, status1) = ParseUrl(input1);
/*
status1              == ParsingStatus.Success
parsed1.Scheme       == UrlScheme.Http
parsed1.Host         == "example.com"
parsed1.Version      == 1
parsed1.PathSegments == [ { Name = "users", Id = 42 } ]
parsed1.QueryParams  == { "lang": ["pl"] }
*/
URL z wieloma segmentami i wielokrotnymi parametrami
var input2 = "https://api.test/v2/orders/100/items/200" +
                "?tag=new&tag=discount&active=true";
var (parsed2, status2) = ParseUrl(input2);
/*
status2              == ParsingStatus.Success
parsed2.Scheme       == UrlScheme.Https
parsed2.Host         == "api.test"
parsed2.Version      == 2
parsed2.PathSegments == [
    { Name = "orders", Id = 100 },
    { Name = "items",  Id = 200 }
]
parsed2.QueryParams  == {
    "tag":    ["new","discount"],
    "active": ["true"]
}
*/
Nieobsługiwany scheme
var input3 = "smtp://host/v1/res/1";
var (_, status3) = ParseUrl(input3);
// status3 == ParsingStatus.InvalidScheme
Nieparzysta liczba segmentów w ścieżce
var input4 = "https://host/v1/resOnly";
var (_, status4) = ParseUrl(input4);
// status4 == ParsingStatus.InvalidPath
Błędny parametr zapytania (brak '=')
var input5 = "http://host/v1/r/10?badparam";
var (_, status5) = ParseUrl(input5);
// status5 == ParsingStatus.InvalidQuery

Zadanie dla chętnych

  • W implementacji metody ParseUrl spróbuj wykorzystać możliwości, jakie daje przestrzeń nazw System.Text.RegularExpressions. Informacje o wyrażeniach regularnych możesz znaleźć m.in. w artykule Microsoft Learn: .NET regular expressions. Do pobrania w formacie .pdf jest również Regular expressions quick reference. Interaktywne tworzenie wyrażeń regularnych zgodnych ze składnią .NET umożliwia m.in. popularna strona regex101.com.

Przykładowe rozwiązanie #

Przykładowe rozwiązanie wraz z testami jednostkowymi można znaleźć w pliku Task01.cs.

Formatowanie, data i czas #

Czym charakteryzuje się format CSV?

CSV (ang. comma-separated values) to format przechowywania danych w plikach tekstowych (odpowiadający mu typ MIME to text/csv).

  • Poszczególne rekordy oddzielone są znakami końca linii \n.
  • Wartości pól standardowo oddzielone są przecinkami ,.
  • Jako separator bywa stosowany znak średnika ; (tak będzie właśnie w naszym zadaniu).
  • W jednym pliku może być użyty tylko jeden rodzaj separatora.
  • Wartości pól mogą być ujęte w cudzysłów (w przypadku wartości zawierających znak separatora jest to wymagane).
  • Pierwsza linia może stanowić nagłówek zawierający nazwy pól rekordów.

Czego się nauczysz?

  • Pracy z datą i czasem w języku C#, odczytując dane z pliku CSV, zawierającego dzienne pomiary temperatury w wybranych europejskich stolicach.
  • Operacji na datach i interwałach czasowych (klasy DateTime i Timespan).
  • Formatowania wyjścia zgodnie z ustawieniami kulturowymi (CultureInfo).
  • Użycia metody ForEach standardowej kolekcji List<T> oraz konstruowania prostych wyrażeń lambda do filtrowania i wyświetlania danych.

Opis zadania #

Twoim zadaniem jest zaimplementowanie metody:

public static List<Measurement> ParseMeasurements(string content);

Metoda powinna:

  • Przyjmować jako parametr ciąg znaków content, będący zawartością pliku CSV.
  • Parsować kolejne rekordy pliku i zwrócić listę obiektów typu Measurement.

Następnie zaimplementuj funkcjonalność wypisywania obiektów klasy Measurement, tak aby ciąg znaków odpowiadający pojedynczemu obiektowi zawierał:

  • Kraj i miasto,
  • Datę w formacie długim zgodnie z kulturą określoną polem Code,
  • Wartości pomiarów sformatowane z separatorami dziesiętnymi i tysięcznymi właściwymi dla kultury.

Przykład wypisania dla rekordu z Code = "pl-PL":

Location: Poland, Warsaw
Date: 21 czerwca 2025
Temperatures:
   0,50 °C   3,20 °C   7,10 °C   12,45 °C
  16,30 °C  14,10 °C  10,05 °C    1,23 °C
   1,23 °C   4,56 °C   7,89 °C

Po sparsowaniu zawartości pliku wypisz obiekty spełniające następujące warunki:

  • Data pomiarów przeprowadzonych w przedziale od 8 czerwca do 13 września bieżącego roku.
  • Data pomiarów przeprowadzonych w roku 2025.
  • Data pomiarów przeprowadzonych tylko w weekendy (sobota i niedziela).
  • Data pomiarów przeprowadzonych w ciągu ostatnich 7 dni.

Definicje typów pomocniczych:

public sealed class Measurement
{
	public string Country { get; set; } = string.Empty;
	public string City { get; set; } = string.Empty;
	public string Code { get; set; } = string.Empty;
	public DateTime Date { get; set; }
	public double[] Temperatures { get; set; } = [];
}

Format pliku CSV

Plik CSV wygląda w następujący sposób:

Location; Code; Date; Temperatures
Poland  Warsaw  ; pl-PL; 2025-06-21; [  25.9, 18.0  , 9.5 , 24.3  ]

Skorzystaj z następującego pliku CSV: measurements.csv.

Uwagi implementacyjne

  • Parsowanie pliku CSV:
    • Do odczytania zawartości pliku skorzystaj z metody File.ReadAllText.
    • Tablica pomiarów zawiera wartości zapisane przy użyciu CultureInfo.InvariantCulture, które są rozdzielone za pomocą znaku przecinka ,.
    • Poszczególne pola rekordów mogą zawierać nadmiarowe białe znaki, których należy się pozbyć.
    • Dla ułatwienia można założyć, że wszystkie rekordy i pola zawierają poprawne dane (np. Date zawiera poprawnie zapisaną datę, a Temperatures poprawnie zapisane liczby zmiennoprzecinkowe).
  • Wypisywanie obiektów:
    • Należy zachować formatowanie z przykładu (format daty, szerokość wypisywania wyrównanie, liczba miejsc po przecinku, liczba pomiarów w jednej linii itp.).
    • Do filtrowania obiektów wykorzystaj metodę ForEach oraz wyrażenia lambda. Nie należy korzystać z jawnej pętli.

Materiały pomocnicze:

Zadanie dla chętnych

Przykładowe rozwiązanie #

Przykładowe rozwiązanie wraz z testami jednostkowymi można znaleźć w pliku Task02.cs.

Liczby losowe, wyrażenia lambda #

Wyrażenia Lambda

Wyrażenia lambda (ang. lambda expressions) to krótkie, anonimowe funkcje, które można zapisać z użyciem operatora =>. Pozwalają one przekazywać logikę jako parametr do innych metod, zwracać funkcje czy przechowywać je w kolekcjach.

Lambdy mogą przechwytywać (ang. closures) zmienne z otaczającego je kontekstu — np. licznik, obiekt klasy Random lub aktualny stan algorytmu — dzięki czemu zachowują dostęp do tych wartości nawet po wyjściu z zakresu, w którym zostały zdefiniowane.

Lambdy są intensywnie wykorzystywane przez metody LINQ m.in. do filtrowania, agregowania, czy tzw. projekcji na określony typ.

Czego się nauczysz?

  • Tworzenia wyrażeń lambda i przekazywania ich w postaci standardowego delegatu Func<T> (funkcje wyższego rzędu).
  • Mechanizmu przechwytywania zmiennych i jego wpływu na działanie kodu.
  • Generowania liczb losowych i podejmowania decyzji z określonym prawdopodobieństwem.

Opis zadania #

Zaimplementuj metodę:

public static void Fill(List<int> collection, int length, Func<int> generator);
  • Metoda dodaje length elementów do listy collection.
  • Każdy kolejny element jest wynikiem wywołania funkcji generator.

Następnie, przy pomocy metody Fill wypełnij i wypisz listy w następujący sposób:

  • 10 kolejnych wyrazów ciągu arytmetycznego o pierwszym wyrazie 3 i różnicy 8.
  • 10 kolejnych elementów ciągu Fibonacciego,
  • 10 losowych liczb z przedziału [5, 50],
  • 10 elementów o wartości 0/1 z zadanym prawdopodobieństwem (np. P(1) = 0.3),
  • 10 losowych elementów ze zbioru 10 początkowych liczb pierwszych [2, 3, 5, 7, 11, 13, 17, 19, 23, 29],
  • Łańcuch Markowa długości 20, ze stanem początkowym 1, określony przez tablicę przejść:
123
10.10.60.3
20.40.20.4
30.50.30.2

Przykładowo dla wiersza 2 i kolumny 1 przejście ze stanu 2 do stanu 1 odbywa się z prawdopodobieństwem 0.5.

Tablicę przejść można przykładowo zaimplementować jako słownik list o elementach będących krotkami postaci (stan, prawdopodobieństwo).

Materiały pomocnicze:

Przykładowe rozwiązanie #

Przykładowe rozwiązanie wraz z testami jednostkowymi można znaleźć w pliku Task03.cs.

Wyrażenia regularne #

Czym są wyrażenia regularne?

Wyrażenia regularne (ang. regular expressions, regex) to potężne narzędzie do wyszukiwania i manipulowania tekstem na podstawie wzorców znaków.

  • Pozwalają na dokładne dopasowanie ciągów znaków w tekście.
  • Umożliwiają grupowanie i przechwytywanie fragmentów dopasowanego tekstu do dalszego przetwarzania.
  • Stosowane są w wielu językach programowania, w tym w C#, gdzie obsługuje je klasa Regex.

Pomimo że ich działanie bywa mniej wydajne niż dedykowane algorytmy (np. oparte na analizie znak po znaku), ich największą zaletą jest zwięzłość i przejrzystość kodu – cała logika dopasowania i ekstrakcji danych może zostać zapisana w jednym wzorcu, a całą pracę wykonuje za nas silnik wyrażeń regularnych. Dzięki temu kod jest czystszy, krótszy i łatwiejszy w utrzymaniu.

Czego się nauczysz?

  • Jak tworzyć i stosować wyrażenia regularne w C# do parsowania złożonych formatów tekstowych, takich jak logi serwera.
  • Jak definiować nazwy grup w wyrażeniach regularnych, aby wygodnie wyodrębniać interesujące dane.

Opis zadania #

Napisz program, który dla podanego pliku tekstowego logs.txt, zawierającego logi w formacie:

[YYYY-MM-DD HH:mm:ss] LEVEL: IP - METHOD /api/RESOURCE/ID - HTTP_CODE HTTP_STATUS[: opcjonalny komunikat]

Użyje wyrażenia regularnego z nazwanymi grupami, aby wyodrębnić następujące pola z każdego wpisu logu:

  • LEVEL: poziom wpisu,
  • RESOURCE: nazwę zasobu,
  • ID: identyfikator zasobu,
  • HTTP_STATUS: kod odpowiedzi,
  • HTTP_STATUS: status odpowiedzi.

Uzyskane informacje powinny zostać zmapowane do kolekcji rekordów LogEntry, a następnie wypisane w konsoli w przykładowym formacie:

LEVEL: RESOURCE/ID => HTTP_CODE HTTP_STATUS

Definicje typów pomocniczych:

public record LogEntry(
	string Level,
	string Resource,
	string Id,
	int HttpCode,
	string HttpStatus
);

Uwagi

  • Zadanie to należy traktować jako dodatkowe (z uwagi na podwyższony poziom trudności i złożoność zagadnienia).

Materiały pomocnicze:

Przykładowe rozwiązanie #

Przykładowe rozwiązanie można znaleźć w pliku Task04.cs.