#ifdef TEST

Dzisiejszy wpis poświęcony będzie tematyce, którą powinienem był poruszyć już ładnych parę tygodni temu – testowaniu warstw dostępu do danych i logiki biznesowej. Zwlekałem z opisaniem tej części projektu, ponieważ planowałem zaprezentować również testy interfejsu użytkownika, których, koniec końców, wcale nie napisałem (pokusa oglądania aplikacji w akcji i sprawdzania wszystkiego własnoręcznie okazała się zbyt duża).
Przejdźmy jednak do rzeczy. Jak już kiedyś wspominałem, solucja InvoiceInvoker składa się z trzech projektów:

  • InvoiceInvoker.Logic – Data Access Layer + Business Logic Layer, z perspektywy czasu stwierdzam, że rozdzielenie tych warstw nie byłoby złym pomysłem,
  • InvoiceInvoker.Logic.Tests – testy jednostkowe pierwszego projektu,
  • InvoiceInvoker.MvcUi – projekt ASP.NET MVC 2, realizujący interakcję z użytkownikiem.

Bohaterem niniejszego wpisu będzie drugi z wymienionych projektów. Zanim przybliżę jego zawartość, opiszę framework testów jednostkowych, z którego korzystam:

NUnit
Testy w NUnit pisze się tak samo, jak zwykłe klasy. Muszą to być klasy publiczne i opatrzone atrybutem TestFixture. Idąc za ciosem, metody testujące to zwykłe metody publiczne z atrybutem Test, zwracające typ void. Przykładowy test klasy wykonującej obliczenia (nazwijmy ją Calculator) wyglądałby więc tak:

using System;
using NUnit.Framework; // przestrzeń nazw NUnit, znajdująca się w bibliotece nunit.framework

namespace SomeProject.Tests
{
	[TestFixture]
	public class CalculatorTests
	{
		[Test]
		public void Adds()
		{
			Calculator calculator = new Calculator(); // utworzenie obiektu testowanej klasy
			int a = 3;
			int b = 5;
			int actual = calculator.Add(a, b); // wynik testowanej metody
			int expected = 8; // spodziewany wynik

			Assert // NUnit.Framework.Assert - klasa, której metody zwracają wynik testu
				.AreEqual( // wynik testu będzie pozytywny, jeśli argumenty będą równe
					expected, actual); // pierwszym argumentem jest spodziewany wynik testowanej metody, drugim - wynik rzeczywisty
		}
	}
}

Gdybyśmy chcieli wykonać ten sam test dla róznych par liczb (nie tylko 3 i 5), metoda Adds mogłaby wyglądać tak:

[Test, Sequential] // Sequential - test zostanie wykonany wielokrotnie
public void Adds([Values(0, 3, -3, 3)] int a, // 0, 3, -3, 3 - kolejne argumenty wywołania testu
	[Values(0, 5, 3, -5)] int b, [Values(0, 8, 0, -2)] int expected)
{
	Calculator calculator = new Calculator();
	int actual = calculator.Add(a, b);

	Assert.AreEqual(expected, actual);
}

Łatwo zauważyć, że „zaszarzona” linia będzie się pojawiać w każdym tego typu teście (Adds, Substracts, Multiplies itp.). Dla takich przypadków istnieje atrybut SetUp:

Calculator calculator; // pole prywatne

[SetUp] // atrybut sprawia, że poniższa metoda zostanie wywołana przed każdym testem
public void TestsSetup()
{
	calculator = new Calculator(); // tę linę można już usunąć z treści metod testujących
}

Może się zdarzyć, że w testach będziemy odwoływać się do pliku konfiguracyjnego testowanego projektu. Warto wiedzieć, że NUnit domyślnie korzysta z pliku konfiguracyjnego [NazwaSolucji].config, mającego się znajdować w katalogu solucji. Odkrycie tej drobnostki zajęło mi dobrą godzinę, wypełnioną zachodzeniem w głowę, dlaczego wynik testu połączenia z bazą danych jest pozytywny, kiedy connection string umieszczony jest „na sztywno” w klasie repozytorium, a negatywny – kiedy jest pobierany z pliku konfiguracyjnego (znajdującego się katalaogu projektu, a nie solucji).

Testy warstwy dostępu do danych
Po przedstawieniu działania NUnit, mogę zaprezentować sposób testowania poszczególnych elementów projektu InvoiceInvoker.Logic. Zacznę od testów warstwy DAL, które umieściłem w przestrzeni nazw InvoiceInvoker.Logic.Tests.RepositoriesTests. Przyznam szczerze, że sposób ich wykonania budzi wiele wątpliwości. Po pierwsze, operacje Get wykonywane są zawsze na encji o identyfikatorze 1, a Update – 2:
(przykład testowania repozytorium sprzedawców – RegisteredSellerRepository)

[Test]
public void GetsById()
{
	RegisteredSeller result = repository.GetById(1); // repository - prywatne pole typu RegisteredSellerRepository

	// sprzedawca o identyfikatorze 1 ma dane takie, jak te podane jako pierwsze argumenty poniższych wywołań:
	Assert.AreEqual("BankAccountNumber", result.BankAccountNumber);
	Assert.AreEqual("BankName", result.BankName);
	Assert.AreEqual("BankSwift", result.BankSwift);
	Assert.AreEqual("City", result.City);
	Assert.AreEqual("CompanyName", result.CompanyName);
	Assert.AreEqual("FirstName", result.FirstName);
	Assert.AreEqual(1, result.Id);
	Assert.AreEqual("InvoiceNumberFormat", result.InvoiceNumberFormat);
	Assert.AreEqual("LastInvoiceNumber", result.LastInvoiceNumber);
	Assert.AreEqual("LastName", result.LastName);
	Assert.AreEqual("NIP", result.Nip);
	Assert.AreEqual("PostalCode", result.PostalCode);
	Assert.AreEqual("REGON", result.Regon);
	Assert.AreEqual("Street", result.Street);
	Assert.AreEqual("UserName", result.UserName);
}

Jeśli dane sprzedawcy o identyfikatorze 1 ulegną zmianie (musiałbym je zmienić ręcznie, jednak istnieje taka możliwość), to test będzie miał wynik negatywny, nawet jeśli metoda GetsById zadziała poprawnie. Test Updates jest wolny od tej wady, ponieważ pobiera encję z bazy, zmienia jej dane, wywołuje metodę Update, a następnie pobiera ją znowu i sprawdza, czy dane się zmieniły. Jednak pobranie encji wiąże się z wywołaniem metody GetById, która nie jest przedmiotem testu – taki test trudno nazwać jednostkowym. Wątpliwości innej natury budzi test dodawania encji – bada tylko, czy liczba wierszy tabeli po wywołaniu metody Add zwiększa się o jeden. Test usuwania łączy dwie ostatnie wady: polega na dodaniu encji (a więc zakłada, że ta operacja działa), usunięciu jej, i sprawdzeniu, czy ilość encji w tabeli nie uległa zmianie.
Czy za napisanie takich testów zasługuję na wieczne potępienie? Jeśli tak, to proszę o niezwłoczne powiadomienie mnie o tym w komentarzu – udam się na wieczną tułaczkę. Na swoją obronę mam tylko tyle, że testy te rzeczywiście pomogły mi wykryć kilka błędów.

Testy generowania dokumentów PDF
Tutaj krótko: nie miałem pojęcia, jak za pomocą kodu sprawdzić, czy wygenerowany plik PDF wygląda jak faktura VAT. Testy polegają więc na wygenerowaniu faktury o określonej właściwości (na przykład zawierającej bardzo długie nazwy produktów) i wyświetleniu jej. Wyniki takich testów zamieściłem na końcu tego wpisu.

Testy logiki biznesowej
Nareszcie testy wolne od kontrowersji. Dotyczą one prawie wszystkich klas opisywanych we wpisach otagowanych jako business logic layer. Dla przykładu, testy klasy zamieniającej liczby na ich reprezentację słowną:

[Test]
public void ConvertsNumbersOf4Digits()
{
	string result = NumberToWordsConverter.Convert(1234);
	Assert.AreEqual("jeden tysiąc dwieście trzydzieści cztery", result);
}

[Test]
public void ConvertsNumbersOf5Digits()
{
	string result = NumberToWordsConverter.Convert(12345);
	Assert.AreEqual("dwanaście tysięcy trzysta czterdzieści pięć", result);
}

// (...)

[Test]
public void ConvertsMoney()
{
	string result = NumberToWordsConverter.ConvertMoney(12.93M, "PLN");
	Assert.AreEqual("dwanaście PLN 93/100", result);

To, co nie budzi kontrowersji, nie budzi też zainteresowania (nie jest to przypadkiem jakieś prawo show-biznesu?) – dlatego pominę szczegółowy opis, dociekliwych odsyłając do kodu źródłowego.

I to by było na tyle. Ten (najdłuższy chyba ze wszystkich) wpis kończy serię wpisów „technicznych”. Zakończenie konkursu zbliża się wielkimi krokami, dlatego pozostałe dwie (bo tyle brakuje do wymaganych dwudziestu) notki poświęcę na podsumowanie tej kilkunastotygodniowej przygody. Zapraszam!

InvoiceInvoker a świat wielkiej polityki i ekonomii

Tytuł wpisu zwiastuje treść i tematykę poważną i podniosłą, jednak bez obaw – poruszana kwestia nie będzie ściśle polityczna czy ekonomiczna, ani daleko odbiegająca od problemów, z którymi zdarza się borykać programistom. Chodzi mianowicie o dostosowanie aplikacji do obowiązujących przepisów, w tym przypadku – stawek procentowych podatku VAT.
Kilka dni po mojej decyzji o starcie w konkursie Daj się poznać z projektem InvoiceInvoker, zapowiedziano zmianę wspomnianych stawek. Był to chyba pierwszy przypadek wywarcia przez urzędujące władze bezpośredniego wpływu na moje decyzje. Ściślej – decyzje projektowe. Dziś, kiedy już prace nad projektem są właściwie na ukończeniu, zaprezentuję tych decyzji konsekwencje – które, co oczywiste dla projektu programistycznego, przybrały postać linii kodu. Przyznaję jednocześnie, że tematyka tego wpisu jest podyktowana moją niechęcią do ciągłego pisania o co raz to nowych widokach i kontrolerach (choć opisanie finalnych widoków i kontrolerów, tych dotyczących wystawiania faktur, wydaje się być nieuniknione).
Przejdźmy jednak do rzeczy. Podstawową decyzją było jak największe uniezależnienie kodu programu od obowiązujących stawek VAT. Początkowo, wprowadzenie tych stawek chciałem zrzucić na użytkownika i umożliwić łatwą ich edycję (na wypadek, gdyby w przyszłości znowu zostały zmienione). Takie rozwiązanie wymagałoby jednak dodania kolejnej kolumny do tabeli sprzedawców w bazie danych, dlatego ostatecznie postanowiłem utworzyć pole VatRates w pliku konfiguracyjnym aplikacji, odpowiedzialne za przechowywanie rzeczonych stawek VAT:

<configuration>
	<appSettings>
		<add key="VatRates" value="22%, 7 %, 3 %, 0 %, n.p., z.w." />
		<add key="PaymentTypes" value="Przelew, Gotówka, Karta płatnicza" /> <!-- swoją drogą, podobnie postąpiłem w kwestii sposobów płatności -->
	</appSettings>

Dostanie się do tego pola i zwrócenie listy stawek nie jest zatem skomplikowane:

public static List<string> GetVatRates()
{
	string vatRates = ConfigurationManager.AppSettings["VatRates"];
	string[] separator = { ", ", "," };
	return vatRates.Split(separator, StringSplitOptions.RemoveEmptyEntries).ToList();
}

Wszystko to jednak tylko ciągi znaków, a przecież głównym ich przeznaczeniem jest reprezentowanie liczb, przez które nalezy pomnożyć wartość netto, by uzyskać wartość brutto produktu. Wyłuskiwaniem tychże liczb zajmuje się następująca metoda:

public static int GetVatPercentage(string vatRate)
{
	int result = 0;
	string vatPercentage = string.Empty;

	if (vatRate.EndsWith(" %")) // ciąg znaków reprezentujący stawkę procentową VAT może kończyć się tylko znakami " %" lub "%"
		vatPercentage = vatRate.Substring(0, vatRate.Length - 2);
	else if (vatRate.EndsWith("%"))
		vatPercentage = vatRate.Substring(0, vatRate.Length - 1);

	// jeśli stawka VAT nie zawiera procentów (jak np. "z.w." - produkt zwolniony od podatku), to jej wartość wynosi 0

	int.TryParse(vatPercentage, out result);

	return result;
}

Wyliczenie wartości podatku VAT odprowadzonego od produktu i warości brutto tego produtku, przy znanej warości netto, wygląda więc tak:

// przedstawione wcześniej metody znajdują się w statycznej klasie VatRatesProvider
int vatPercentage = VatRatesProvider.GetVatPercentage(product.VatRate); //właściwość VatRate jest ciągiem znaków zawierającym stawkę VAT produktu
product.VatValue = product.NetValue * (vatPercentage * 0.01M); // NetValue - wartość netto produktu, VatValue - warość podatku
product.GrossValue = product.NetValue + product.VatValue; // GrossValue - wartość brutto produktu

Metody GetVatRates natomiast używam do przekazania stawek VAT widokom tworzenia i edycji produktów.

I to wszystko w tym temacie, przedstawione rozwiązanie nie jest skomplikowane, ale chyba warte opisania.
Na koniec małe usprawiedliwienie. Wraz z nadejściem (a niedawno także odejściem) ostatnich dni wakacji, częstotliwość pojawiania się nowych notek zauważalnie spadła, czego powodem, jak łatwo się domyślić, jest natłok wszelakich zajęć różnych od kodowania. Projektowi nie grozi jednak, jeśli dobrze rozumiem regulamin i FAQ konkursu, dyskwalifikacja: jeśli dobrze rozumiem regulamin i FAQ konkursu, bardziej niż częstotliwość wpisów, liczy się ich ilość – która do piętnastego listopada powinna liczyć co najmniej dzwadzieścia.

Postać Docelowa Faktury

Po dwutygodniowym rejsie po Mazurach, czas wrócić do pracy nad projektem. Aby skończyć warstwę logiki biznesowej, muszę jeszcze tylko stworzyć klasę, która zajmie się zamianą przyczajonej w bazie danych, zdigitalizowanej do cna postaci faktury w nie mniej zdigitalizowaną, ale niepomiernie bardziej czytelną dla użytkownika programu, postać dokumentu PDF. Użyję w tym celu biblioteki PdfSharp.

BHP z ostrym narzędziem
Tworzenie dokumentów PDF przy pomocy PdfSharp nie jest trudne. Przygotowanie nowego dokumentu to tylko kilka linijek:

PdfDocument document = new PdfDocument(); // stworzenie nowego dokumentu
PdfPage page = document.AddPage(); // stworzenie nowej strony dokumentu
XGraphics graphics = XGraphics.FromPdfPage(page); // stworzenie obiektu odpowiedzialnego za wygląd strony

Obiektem odpowiedzialnym za umieszczanie grafiki i treści na stronach dokumentu jest instancja klasy XGraphics. Na wygląd faktury będą się składać wyłącznie prostokąty i tekst, dlatego przedstawię sposób generowania tylko tych elementów.
Za tworzenie prostokątów odpowiada metoda DrawRectangle. Przykładowo, kod:

XPen pen = new XPen(XColors.Black, 1); // obramowanie prostokąta
XBrush brush = XBrushes.Red; // wypełnienie prostokąta
XRect rect = new XRect(0, 0, 60, 20); // położenie i wymiary prostokąta (x, y, szerokość, wysokość)
graphics.DrawRectangle(pen, brush, rect); // narysowanie prostokąta

narysuje w lewym górnym rogu strony czerwony prostokąt z czarnym obramowaniem.
Umieszczaniem na stronie tekstu zajmuje się metoda DrawString. I tak, kod:

XFont font = new XFont("Arial", 10, XFontStyle.Bold); // krój, rozmiar i styl czcionki
graphics.DrawString("Poufne", font, XBrushes.Black, rect, XStringFormats.Center); // dodanie czarnego napisu w środku stworzonego wcześniej prostokąta

sprawi, że nasz dokument, niezależnie od dalszej treści, będzie mógł być wykorzystany jako rekwizyt w dowolnym filmie szpiegowskim.

Przyda mi się także metoda MeasureString, zwracająca rozmiar napisu pisanego zadaną czcionką:

XSize size = graphics.MeasureString("<napis do zmierzenia>", czcionka);

Zapisanie dokumentu na dysku to nic więcej, niż napisanie:

document.Save("<nazwa pliku>");

Te metody (i ich przeciążenia) w zupełności wystarczą do wystawienia faktury VAT.

Very Arduous Task
Przepisy określają informacje, jakie muszą się znaleźć na fakturze:

  • nazwy i adresy sprzedawcy i nabywcy oraz ich numery NIP
  • datę dokonania sprzedaży oraz datę wystawienia dokumentu
  • napis Faktura VAT oraz numer kolejny faktury
  • nazwę towaru lub usługi (przedmiotu transakcji)
  • jednostkę miary i ilość sprzedanych towarów lub rodzaj wykonanych usług
  • cenę jednostkową towaru lub usługi bez kwoty podatku (cenę jednostkową netto)
  • wartość towarów lub wykonanych usług, których dotyczy sprzedaż, bez kwoty podatku (wartość sprzedaży netto)
  • stawki podatku
  • sumę wartości sprzedaży netto towarów lub wykonanych usług z podziałem na poszczególne stawki podatku, zwolnionych z podatku oraz niepodlegających opodatkowaniu
  • kwotę podatku od sumy wartości sprzedaży netto, z podziałem na kwoty dotyczące poszczególnych stawek podatku
  • wartość sprzedaży towarów lub wykonanych usług wraz z kwotą podatku (wartość sprzedaży brutto), z podziałem na kwoty dotyczące poszczególnych stawek podatku, zwolnionych z podatku lub niepodlegających opodatkowaniu
  • kwotę należności ogółem wraz z należnym podatkiem (brutto), wyrażoną cyframi i słownie
  • źródło: wikipedia

    Nie opisują jednak, jak dokładnie faktury mają wyglądać. Te wystawiane za pomocą InvoiceInvoker będą podobne do tworzonych przez program inFakt (jak już wcześniej wspominałem, zamierzam się czasem na nim wzorować), co, mam nadzieję, nie jest wykroczeniem. Po wielu godzinach tworzenia prostokątów, zmieniania ich rozmiarów i przesuwania o piksel, a także wypełniania tekstem o żmudnie dopasowywanych czcionkach, uzyskałem kod generujący akceptowalnie wyglądające dokumenty. Oto wyniki testów:
    [uwaga: arytmetyka nie była przedmiotem testów, dlatego nie należy szukać sensu w liczbach widniejących na fakturach]
    test_CreatesNormalInvoice
    test_HandlesVeryLongStrings
    test_SplitsProducts
    test_SplitsGeneralAmountsInfo
    test_SplitsPaymentInfo
    test_SplitsRemarks
    test_SplitsSignatures.

    Kodu klasy nie zamieszczam z oczywistego powodu: jest długi i nudny. Wytrwałym śmiałkom podpowiem jednak, że można go znaleźć tutaj. Wszystkim, którzy szukają sposobu na przeistoczenie kodu w dokument PDF, mogę natomiast polecić dokładniejsze zapoznanie się z PdfSharp.
    Wygląda na to, że warstwa logiki biznesowej jest już ukończona. Wkraczam więc na niepewny grunt: ASP.NET MVC, którego opanowanie jest jednym z głównych celów mojego udziału w konkursie. Stay tuned!

    Numer kolejny faktury

    Punkt drugi założeń projektowych (które można znaleźć tutaj) głosi:
    2. numer faktury definiowany szablonem,
    Czas zająć się tą funkcjonalnością. Projektując bazę danych zdecydowałem, że zarówno numer faktury jak i jego szablon będą łańcuchami znaków. Spójrzmy na przykład: „1/08/2010” – numer pierwszej faktury w sierpniu 2010. Według reguł, które zaraz przedstawię, szablon takiego numeru wygląda następująco: „N/MM/RR”.

    Struktura szablonu numeru faktury
    Numer kolejny faktury, jak można wywnioskować ze wstępu, powinien zawierać:
    – informację (zwaną dalej numerem) mówiącą, którą fakturą w danym miesiącu jest dana faktura,
    – miesiąc wystawienia faktury,
    – rok wystawienia faktury.
    Każdemu z tych elementów przypisałem literę: ‚N’ oznacza numer, ‚M’ – miesiąc, ‚R’ – rok. Użytkownik powinien móc zdecydować, czy miesiąc (przykładowo) sierpień będzie oznaczany jako „8”, czy „08”, i czy rok 2010 to „2010”, czy „10”. Podobnie, to od widzimisię użytkownika powinno zależeć, czy pierwsza faktura danego miesiąca otrzyma numer „1”, „01”, czy może „001”. Wszystkie te kwestie rozwiązałem w prosty i, mam nadzieję, intuicyjny sposób:
    – pojawienie się „M” w formacie oznacza, że (przykładowo) sierpień zostanie zapisany jako „8”,
    – pojawienie się „MM” – jako „08”,
    – „R” – rok 2010 zostanie zapisany jako „10”,
    – „RR” – jako „2010”,
    – „N” – pierwsza faktura miesiąca otrzyma numer „1”,
    – „NN” – numer „01”,
    – „NNN” – numer „001”, „NNNN” – „0001” i tak dalej.
    Aby nie było bałaganu, numer, miesiąc i rok mogą (i muszą) wystąpić tylko raz – poprawany jest szablon „N/M/R”, a niepoprawne: „N/R”, „N/M/R/R”. Co więcej, symbole te muszą być oddzielone separatorami, na przykład, jak we wszystkich powyższych przykładach, slashem.
    Klasę obsługującą numery faktur nazwałem InvoiceNumber. Działa ona w obie strony (tworzy numer kolejny faktury zgodny z zadanym szablonem na podstawie numeru, miesiąca i daty, a także odczytuje numer, miesiąc i datę na podstawie numeru kolejnego i jego szablonu) i wygląda tak:

    using System.Collections.Generic;
    using System.Linq;
    
    namespace InvoiceInvoker.Logic
    {
    	public class InvoiceNumber
    	{
    		public int Number { get; private set; }
    		public int Month { get; private set; }
    		public int Year { get; private set; }
    		public string Format { get; private set; }
    		public List<string> formatArray = new List<string>();
    
    		public InvoiceNumber(int number, int month, int year, string format = "N/MM/RR")
    		{
    			Number = number;
    			Month = month;
    			Year = year;
    			Format = CheckFormat(format) ? format : "N/MM/RR"; // default format is N/MM/RR
    			SetFormatArray();
    		}
    
    		public InvoiceNumber(string invoiceNumber, string format = "N/MM/RR")
    		{
    			Format = CheckFormat(format) ? format : "N/MM/RR"; // default format is N/MM/RR
    			SetFormatArray();
    			ReadFromString(invoiceNumber);
    		}
    
    		public static bool CheckFormat(string format)
    		{
    			// examples of proper formats: "N/M/R", "RR-MM-NN"
    			// N - invoice number
    			// M - month
    			// R - year
    
    			// format must contain letters: N, M, R
    			if (format.Contains('N') == false) return false;
    			if (format.Contains('M') == false) return false;
    			if (format.Contains('R') == false) return false;
    
    			// format cannot contain digits
    			if (format.Any(c => char.IsDigit(c))) return false;
    
    			int firstIndexOfN = format.IndexOf('N');
    			int lastIndexOfN = format.LastIndexOf('N');
    			int firstIndexOfM = format.IndexOf('M');
    			int lastIndexOfM = format.LastIndexOf('M');
    			int firstIndexOfR = format.IndexOf('R');
    			int lastIndexOfR = format.LastIndexOf('R');
    
    			// letters N must form a sequence, e.g. "NNN/M/R"
    			string sequence = format.Substring(firstIndexOfN, lastIndexOfN - firstIndexOfN + 1);
    			if (sequence.Any(c => c != 'N')) return false;
    
    			// letters M, R can only occur in the following combinations:
    			// ...X...
    			// ...XX...
    			// e.g. formats "N/M/M/R", "N:M:MM:R" are not allowed
    			if (lastIndexOfM - firstIndexOfM > 1) return false; // allows only one occurance of the following: "...N...", "...NN..."
    			if (lastIndexOfR - firstIndexOfR > 1) return false;
    
    			// letters N, M, R cannot occur next to each other
    			if (firstIndexOfM - lastIndexOfN == 1) return false; // disqualifies "NM" sequence
    			if (firstIndexOfN - lastIndexOfM == 1) return false; // disqualifies "MN" sequence
    			if (firstIndexOfR - lastIndexOfM == 1) return false; // disqualifies "MR" sequence
    			if (firstIndexOfM - lastIndexOfR == 1) return false; // disqualifies "RM" sequence
    			if (firstIndexOfR - lastIndexOfN == 1) return false; // disqualifies "NR" sequence
    			if (firstIndexOfN - lastIndexOfR == 1) return false; // disqualifies "RN" sequence
    
    			return true;
    		}
    
    		private void SetFormatArray()
    		{
    			formatArray.Add(Format[0].ToString());
    
    			for (int i = 1; i != Format.Length; i++)
    			{
    				if (Format[i] == 'N' || Format[i] == 'M' || Format[i] == 'R') // current char is a symbol of number
    				{
    					if (Format[i] == Format[i - 1]) // current char is a part of number sequence
    						formatArray[formatArray.Count - 1] += Format[i];
    					else							// current char starts a number sequence
    						formatArray.Add(Format[i].ToString());
    				}
    				else														  // current char is a symbol of separator
    				{
    					if (Format[i - 1] == 'N' || Format[i - 1] == 'M' || Format[i - 1] == 'R') // current char starts a separator sequence
    						formatArray.Add(Format[i].ToString());
    					else																	  // current char is a part of separator sequence
    						formatArray[formatArray.Count - 1] += Format[i];
    				}
    			}
    		}
    
    		private void ReadFromString(string invoiceNumber)
    		{
    			string temp = invoiceNumber;
    
    			for (int index = 0; index != formatArray.Count; index++)
    			{
    				if (formatArray[index].Contains('N')) // section under the index is the number section
    				{
    					string number = string.Empty;
    
    					// e.g. if temp == "11/12/13" ...
    					while (temp.Length > 0 && char.IsDigit(temp[0]))
    					{
    						number += temp[0];
    						temp = temp.Remove(0, 1);
    					}
    					// ... then number == "11" and temp == "/12/13"
    
    					Number = int.Parse(number);
    				}
    				else if (formatArray[index].Contains('M')) // section under the index is the month section
    				{
    					string month = string.Empty;
    
    					while (temp.Length > 0 && char.IsDigit(temp[0]))
    					{
    						month += temp[0];
    						temp = temp.Remove(0, 1);
    					}
    
    					Month = int.Parse(month);
    				}
    				else if (formatArray[index].Contains('R')) // section under the index is the year section
    				{
    					string year = string.Empty;
    
    					while (temp.Length > 0 && char.IsDigit(temp[0]))
    					{
    						year += temp[0];
    						temp = temp.Remove(0, 1);
    					}
    
    					Year = int.Parse(year);
    
    					if (formatArray[index] == "R") // e.g if year in string is "10" ...
    						Year += 2000;			   // ... then the property should be 2010
    				}
    				else									   // section under the index is a separator section
    				{
    					temp = temp.Remove(0, formatArray[index].Length);
    				}
    			}
    		}
    
    		public override string ToString()
    		{
    			string result = string.Empty;
    
    			for (int index = 0; index != formatArray.Count; index++)
    			{
    				if (formatArray[index].Contains('N')) // section under the index is the number section
    				{
    					int amountOfZeros = formatArray[index].Length - Number.ToString().Length; // e.g. if formatArray[index] == "NNNNN" and Number == 1 ...
    					for (int i = 0; i < amountOfZeros; i++)
    						result += "0";
    
    					result += Number.ToString();											  // ... then the number is converted into "00001"
    				}
    				else if (formatArray[index] == "M") // section under the index is the month section
    				{
    					result += Month.ToString();
    				}
    				else if (formatArray[index] == "MM") // section under the index is the month section
    				{
    					if (Month < 10)				// e.g. if Month == 1 ...
    						result += "0";
    
    					result += Month.ToString(); // ... then it's converted into "01"
    				}
    				else if (formatArray[index] == "R") // section under the index is the year section
    				{
    					int year = Year - 2000;	   // e.g. if Year == 2001 ...
    
    					if (year < 10)
    						result += "0";
    
    					result += year.ToString(); // ... then it's converted into "01"
    				}
    				else if (formatArray[index] == "RR") // section under the index is the year section
    				{
    					result += Year.ToString();
    				}
    				else								 // section under the index is a separator section
    				{
    					result += formatArray[index];
    				}
    			}
    
    			return result;
    		}
    	}
    }
    

    Kolejną funkcjonalnością będzie najprawdopodobniej graficzna reprezentacja faktury w formacie PDF – ale o tym we wrześniu (patrz ostatni akapit poprzedniego wpisu).

    Słownie złotych:

    Zgodnie z obowiązującym w Polsce prawem, faktura powinna zawierać przynajmniej:
    (…)
    – kwotę należności ogółem wraz z należnym podatkiem (brutto), wyrażoną cyframi i słownie.

    Tak podaje wikipedia. Dziś zajmę się pogrubionym fragmentem cytatu: zamianą kwoty pieniędzy określonej waluty na jej słowną reprezentację. Zakładam, że wystawiając faktury na kwoty powyżej 999999,99 (czyli milion i więcej) jednostek monetarnych, użytkownik – z nieopisaną satysfakcją – wpisze kwotę słownie własnoręcznie.

    Liczby => słowa
    Na początek zajmę się zamianą liczb całkowitych na słowa. Zastosuję poniższy algorytm:
    1. podział liczby na grupy cyfr (przykładowo liczbę 123456 podzielę na 123 i 456, a 12345 – na 12 i 345),
    2. zamiana bardziej znaczącej grupy na słowa,
    3. dodanie słowa „tysiące” w odpowiedniej formie,
    4. zamiana mniej znaczącej grupy na słowa.

    W implementacji algorytmu przyda mi się extension method zamieniająca liczbę na wektor jej cyfr:

    private static int[] ToDigitArray(this int number)
    {
    	// przykładowo zamienia 123456 na { 6, 5, 4, 3, 2, 1 } 
    	// (najmniej znacząca cyfra -> indeks 0)
    
    	string str = number.ToString();
    	int[] digitArray = new int[str.Length];
    
    	for (int i = 0; i != digitArray.Length; i++)
    		digitArray[i] = int.Parse(str[str.Length - 1 - i].ToString());
    
    	return digitArray;
    }
    

    Metody zamieniające kolejne cyfry na słowa wyglądają, tu fajerwerków nie będzie, tak:
    (zamienia jedności:)

    private static string ConvertUnits(int number)
    {
    	switch (number)
    	{
    		case 1: return "jeden";
    		case 2: return "dwa";
    		case 3: return "trzy";
    		case 4: return "cztery";
    		case 5: return "pięć";
    		case 6: return "sześć";
    		case 7: return "siedem";
    		case 8: return "osiem";
    		case 9: return "dziewięć";
    		default: return string.Empty;
    	}
    }
    

    (zamienia „nastki”:)

    private static string ConvertTeens(int number)
    {
    	switch (number)
    	{
    		case 11: return "jedenaście";
    		case 12: return "dwanaście";
    		case 13: return "trzynaście";
    		case 14: return "czternaście";
    		case 15: return "pięnaście";
    		case 16: return "szesnaście";
    		case 17: return "siedemnaście";
    		case 18: return "osiemnaście";
    		case 19: return "dziewiętnaście";
    		default: return string.Empty;
    	}
    }
    

    (zamienia dziesiątki:)

    private static string ConvertTens(int number)
    {
    	switch (number)
    	{
    		case 1: return "dziesięć";
    		case 2: return "dwadzieścia";
    		case 3: return "trzydzieści";
    		case 4: return "czterdzieści";
    		case 5: return "pięćdziesiąt";
    		case 6: return "sześćdziesiąt";
    		case 7: return "siedemdziesiąt";
    		case 8: return "osiemdziesiąt";
    		case 9: return "dziewięćdziesiąt";
    		default: return string.Empty;
    	}
    }
    

    (zamienia setki:)

    private static string ConvertHundreds(int number)
    {
    	switch (number)
    	{
    		case 1: return "sto";
    		case 2: return "dwieście";
    		case 3: return "trzysta";
    		case 4: return "czterysta";
    		case 5: return "pięćset";
    		case 6: return "sześćset";
    		case 7: return "siedemset";
    		case 8: return "osiemset";
    		case 9: return "dziewięćset";
    		default: return string.Empty;
    	}
    }
    

    Zamiana grup cyfr (o których mowa w pkt. 1 algorytmu) na słowa wygląda więc tak,

    private static string GetHundreds(int[] numberArray)
    {
    	string result;
    
    	switch (numberArray.Length)
    	{
    		case 1:	// np. 9
    			result = ConvertUnits(numberArray[0]);
    			break;
    		case 2: // np. 99
    			if (numberArray[1] == 1) // np. 91
    				result = ConvertTeens(10 + numberArray[0]);
    			else
    				result = ConvertTens(numberArray[1]) + " " + ConvertUnits(numberArray[0]);
    			break;
    		case 3: // np. 999
    			if (numberArray[1] == 1) // np. 919
    				result = ConvertHundreds(numberArray[2]) + " " + ConvertTeens(10 + numberArray[0]);
    			else
    				result = ConvertHundreds(numberArray[2]) + " " + ConvertTens(numberArray[1]) + " " + ConvertUnits(numberArray[0]);
    			break;
    		default:
    			result = string.Empty;
    			break;
    	}
    
    	return result;
    }
    

    metoda używana przy trzecim kroku algorytmu – tak,

    private static string GetGrammaticalThousands(int lastDigit)
    {
    	switch (lastDigit)
    	{
    		case 1:
    			return "tysiąc";
    		case 2:
    		case 3:
    		case 4:
    			return "tysiące";
    		default:
    			return "tysięcy";
    	}
    }
    

    a cały algorytm – tak (number to liczba pobrana w argumencie):

    string result = string.Empty;
    int[] digitArray = number.ToDigitArray();
    
    if (digitArray.Length == 6) // e.g. 123456
    {
    	int[] moreSignificantDigits = { digitArray[3], digitArray[4], digitArray[5] }; // e.g. { 3, 2, 1 }
    	int[] lessSignificantDigits = { digitArray[0], digitArray[1], digitArray[2] }; // e.g. { 6, 5, 4 }
    
    	if (digitArray[4] == 1) // e.g. 919999
    		result = GetHundreds(moreSignificantDigits) + " tysięcy " + GetHundreds(lessSignificantDigits);
    	else
    		result = GetHundreds(moreSignificantDigits) + " " + GetGrammaticalThousands(digitArray[3]) + " " + GetHundreds(lessSignificantDigits);
    }
    else if (digitArray.Length == 5) // e.g. 12345
    {
    	int[] moreSignificantDigits = { digitArray[3], digitArray[4] }; // e.g. { 2, 1 }
    	int[] lessSignificantDigits = { digitArray[0], digitArray[1], digitArray[2] }; // e.g. { 5, 4, 3 }
    
    	if (digitArray[4] == 1) // 91999
    		result = GetHundreds(moreSignificantDigits) + " tysięcy " + GetHundreds(lessSignificantDigits);
    	else
    		result = GetHundreds(moreSignificantDigits) + " " + GetGrammaticalThousands(digitArray[3]) + " " + GetHundreds(lessSignificantDigits);
    }
    else if (digitArray.Length == 4) // e.g. 1234
    {
    	int[] moreSignificantDigits = { digitArray[3] }; // e.g. { 4 }
    	int[] lessSignificantDigits = { digitArray[0], digitArray[1], digitArray[2] }; // e.g. { 3, 2, 1 }
    
    	result = GetHundreds(moreSignificantDigits) + " " + GetGrammaticalThousands(digitArray[3]) + " " + GetHundreds(lessSignificantDigits);
    }
    else
    {
    	result = GetHundreds(digitArray);
    }
    

    Dla number == 0 metoda konwertująca (o niezbyt zaskakującej nazwie Convert) zwraca „zero”, a dla number >= 1,000,000 – „wpisz ręcznie”.

    Kwota => słowa
    Zamiana kwoty pieniężnej na słowa to już sprawa prosta:

    public static string ConvertMoney(decimal amount, string currency)
    {
    	string result = Convert((int)amount); // konwersja części całkowitej
    	result += " " + currency + " "; // dodanie waluty
    	decimal decimals = (amount - decimal.Floor(amount)) * 100; // wyłuskanie części ułamkowej
    	result += ((int)decimals).ToString(); // dodanie części ułamkowej
    	result += "/100";
    
    	return result;
    }
    

    W ten sposób kwota 123456,78 PLN jest zamieniana na: „sto dwadzieścia trzy tysiące czterysta pięćdziesiąt sześć PLN 78/100”.

    Na koniec małe usprawiedliwienie. Nie można zaprzeczyć, że wpis jest rażąco lakoniczy i, co tu kryć, pisany na szybko. Jest to spowodowane tym, że aktualnie się przeprowadzam. Z jednego miejsca zamieszkania – w trzy (szczegóły tej sytuacji ciężko byłoby opisać w kilku słowach…). Dodatkowo, post ten napisałem na kilka dni przed jego opublikowaniem, ponieważ w chwili, gdy czytelnik beznamiętnie przewija kolejne kawały zaprezentowanego przeze mnie kodu, ja beztrosko bujam się na falach któregoś z jezior mazurskich. Taki natłok wydarzeń uniemożliwia mi regularne blogowanie, dlatego czeka mnie przynajmniej tydzień „urlopu” od konkursu.

    Księga liczb: walidacja NIPu i REGONu

    Po ukończeniu warstwy dostępu do danych, mogę zabrać się za logikę biznesową. Na początek – walidacja numerów NIP i REGON.
    Czym jest NIP, wie mniej więcej każdy. Czym jest REGON, zapewne bardziej „mniej” niż „więcej”. Z kolei wiedzę o tym, co oznaczają poszczególne cyfry tych numerów, moża śmiało uznać za tajemną. Dziś, na potrzeby projektu, uchylę rąbka tajemnicy: zajmę się ich ostatnimi cyframi. Są to cyfry kontrolne, wyznaczane algorytmicznie. Aby je wyliczyć, należy:
    1. pomnożyć każdą z poprzednich cyfr przez odpowiednie wagi,
    2. zsumować wyniki mnożeń,
    3. obliczyć resztę z dzielenia przez 11.

    NIP
    Przyjrzyjmy się bliżej numerowi NIP. Aby określić, czy ciąg znaków jest poprawnym Numerem Identyfikacji Podatkowej, należy przede wszystkim sprawdzić, czy w ogóle jest numerem (linia nr 5 kodu, który zaraz przedstawię). Konkretniej: numerem dziesięciocyfrowym (linia nr 7). Dopiero teraz można zastosować wyżej przedstawiony algorytm – wspomniane w punkcie pierwszym wagi to kolejno: 6, 5, 7, 2, 3, 4, 5, 6, 7. Programistom wygodniej będzie spojrzeć na kod:

    public static bool ValidateNip(string nipString)
    {
    	long nip;
    
    	if (long.TryParse(nipString, out nip))
    	{
    		int[] digitArray = nip.ToDigitArray();
    
    		if (digitArray.Length == 10)
    		{
    			int sum = digitArray[0] * 6
    					+ digitArray[1] * 5
    					+ digitArray[2] * 7
    					+ digitArray[3] * 2
    					+ digitArray[4] * 3
    					+ digitArray[5] * 4
    					+ digitArray[6] * 5
    					+ digitArray[7] * 6
    					+ digitArray[8] * 7;
    
    			if (sum % 11 == digitArray[9])
    				return true;
    		}
    	}
    
    	return false;
    }
    

    W podświetlonej (zaciemnionej?) linii używam extension method zamieniającej liczbę na wektor jej cyfr:

    private static int[] ToDigitArray(this long number)
    {
    	// przykładowo zamienia 123456 na { 1, 2, 3, 4, 5, 6 }
    	// (najbardziej znacząca cyfra -> indeks 0)
    
    	string str = number.ToString();
    	int[] digitArray = new int[str.Length];
    
    	for (int i = 0; i != digitArray.Length; i++)
    		digitArray[i] = int.Parse(str[i].ToString());
    
    	return digitArray;
    }
    

    REGON
    Numer Rejestru Gospodarki Narodowej może składać się z dziewięciu lub czternastu cyfr. Wagi, o których mowa w algorytmie wyznaczania cyfry kontrolnej, wynoszą odpowiednio:
    1. 8, 9, 2, 3, 4, 5, 6, 7 dla REGONu 9-cyfrowego,
    2. 2, 4, 8, 5, 0, 9, 7, 3, 6, 1, 2, 4, 8 dla REGONu – 14-cyfrowego.
    Cały proces sprawdzania popraności tego numeru wygląda więc tak:

    public static bool ValidateRegon(string regonString)
    {
    	long regon;
    
    	if (long.TryParse(regonString, out regon))
    	{
    		int[] digitArray = regon.ToDigitArray();
    
    		if (digitArray.Length == 9)
    		{
    			int sum = digitArray[0] * 8
    					+ digitArray[1] * 9
    					+ digitArray[2] * 2
    					+ digitArray[3] * 3
    					+ digitArray[4] * 4
    					+ digitArray[5] * 5
    					+ digitArray[6] * 6
    					+ digitArray[7] * 7;
    
    			int mod = sum % 11;
    
    			if (mod == 10)
    				mod = 0;
    
    			if (mod == digitArray[8])
    				return true;
    		}
    		else if (digitArray.Length == 14)
    		{
    			int sum = digitArray[0] * 2
    					+ digitArray[1] * 4
    					+ digitArray[2] * 8
    					+ digitArray[3] * 5
    					+ digitArray[4] * 0
    					+ digitArray[5] * 9
    					+ digitArray[6] * 7
    					+ digitArray[7] * 3
    					+ digitArray[8] * 6
    					+ digitArray[9] * 1
    					+ digitArray[10] * 2
    					+ digitArray[11] * 4
    					+ digitArray[12] * 8;
    
    			int mod = sum % 11;
    
    			if (mod == 10)
    				mod = 0;
    
    			if (mod == digitArray[13])
    				return true;
    		}
    	}
    
    	return false;
    }
    

    Jak widać, pierwsze kroki po warstwie logiki biznesowej to raczej spacer, niż bieg przez płotki. Na horyzoncie majaczą się jednak takie problemy, jak zamiana liczb na słowa, czy obsługa szablonów numerów faktur – o tym w przyszłych wpisach, zapraszam!