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).

1 thought on “Numer kolejny faktury”

Comments are closed.