csv-detective 0.10.3.dev7__py3-none-any.whl → 0.10.2549__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. csv_detective/detection/__init__.py +0 -0
  2. csv_detective/detection/columns.py +0 -0
  3. csv_detective/detection/encoding.py +0 -0
  4. csv_detective/detection/engine.py +0 -0
  5. csv_detective/detection/formats.py +38 -11
  6. csv_detective/detection/headers.py +14 -12
  7. csv_detective/detection/rows.py +1 -1
  8. csv_detective/detection/separator.py +0 -0
  9. csv_detective/detection/variables.py +0 -0
  10. csv_detective/explore_csv.py +6 -18
  11. csv_detective/format.py +5 -12
  12. csv_detective/formats/__init__.py +0 -0
  13. csv_detective/formats/adresse.py +9 -9
  14. csv_detective/formats/binary.py +1 -2
  15. csv_detective/formats/booleen.py +2 -3
  16. csv_detective/formats/code_commune_insee.py +10 -12
  17. csv_detective/formats/code_csp_insee.py +1 -1
  18. csv_detective/formats/code_departement.py +7 -8
  19. csv_detective/formats/code_fantoir.py +5 -6
  20. csv_detective/formats/code_import.py +1 -1
  21. csv_detective/formats/code_postal.py +9 -10
  22. csv_detective/formats/code_region.py +6 -7
  23. csv_detective/formats/code_rna.py +6 -7
  24. csv_detective/formats/code_waldec.py +1 -1
  25. csv_detective/formats/commune.py +5 -5
  26. csv_detective/formats/csp_insee.py +5 -6
  27. csv_detective/formats/data/insee_ape700.txt +1 -1
  28. csv_detective/formats/data/iso_country_code_alpha2.txt +397 -153
  29. csv_detective/formats/data/iso_country_code_alpha3.txt +132 -132
  30. csv_detective/formats/data/iso_country_code_numeric.txt +94 -94
  31. csv_detective/formats/date.py +18 -28
  32. csv_detective/formats/date_fr.py +1 -1
  33. csv_detective/formats/datetime_aware.py +2 -7
  34. csv_detective/formats/datetime_naive.py +0 -3
  35. csv_detective/formats/datetime_rfc822.py +0 -1
  36. csv_detective/formats/departement.py +15 -15
  37. csv_detective/formats/email.py +13 -13
  38. csv_detective/formats/float.py +1 -2
  39. csv_detective/formats/geojson.py +10 -10
  40. csv_detective/formats/insee_ape700.py +8 -10
  41. csv_detective/formats/insee_canton.py +6 -6
  42. csv_detective/formats/int.py +1 -2
  43. csv_detective/formats/iso_country_code_alpha2.py +14 -14
  44. csv_detective/formats/iso_country_code_alpha3.py +13 -6
  45. csv_detective/formats/iso_country_code_numeric.py +9 -2
  46. csv_detective/formats/jour_de_la_semaine.py +12 -11
  47. csv_detective/formats/json.py +0 -6
  48. csv_detective/formats/latitude_l93.py +22 -8
  49. csv_detective/formats/latitude_wgs.py +29 -31
  50. csv_detective/formats/latitude_wgs_fr_metropole.py +30 -7
  51. csv_detective/formats/latlon_wgs.py +28 -30
  52. csv_detective/formats/longitude_l93.py +13 -8
  53. csv_detective/formats/longitude_wgs.py +19 -34
  54. csv_detective/formats/longitude_wgs_fr_metropole.py +19 -6
  55. csv_detective/formats/lonlat_wgs.py +11 -12
  56. csv_detective/formats/mois_de_lannee.py +1 -1
  57. csv_detective/formats/money.py +1 -1
  58. csv_detective/formats/mongo_object_id.py +1 -1
  59. csv_detective/formats/pays.py +13 -11
  60. csv_detective/formats/percent.py +1 -1
  61. csv_detective/formats/region.py +13 -13
  62. csv_detective/formats/sexe.py +1 -1
  63. csv_detective/formats/siren.py +10 -9
  64. csv_detective/formats/siret.py +9 -9
  65. csv_detective/formats/tel_fr.py +13 -7
  66. csv_detective/formats/uai.py +18 -17
  67. csv_detective/formats/url.py +16 -16
  68. csv_detective/formats/username.py +1 -1
  69. csv_detective/formats/uuid.py +1 -1
  70. csv_detective/formats/year.py +12 -7
  71. csv_detective/output/__init__.py +0 -0
  72. csv_detective/output/dataframe.py +3 -8
  73. csv_detective/output/example.py +0 -0
  74. csv_detective/output/profile.py +2 -6
  75. csv_detective/output/schema.py +0 -0
  76. csv_detective/output/utils.py +0 -0
  77. csv_detective/parsing/__init__.py +0 -0
  78. csv_detective/parsing/columns.py +1 -1
  79. csv_detective/parsing/compression.py +0 -0
  80. csv_detective/parsing/csv.py +0 -0
  81. csv_detective/parsing/excel.py +1 -1
  82. csv_detective/parsing/load.py +12 -11
  83. csv_detective/parsing/text.py +12 -13
  84. csv_detective/validate.py +36 -71
  85. {csv_detective-0.10.3.dev7.dist-info → csv_detective-0.10.2549.dist-info}/METADATA +18 -15
  86. csv_detective-0.10.2549.dist-info/RECORD +92 -0
  87. csv_detective-0.10.2549.dist-info/WHEEL +4 -0
  88. {csv_detective-0.10.3.dev7.dist-info → csv_detective-0.10.2549.dist-info}/entry_points.txt +1 -0
  89. csv_detective-0.10.3.dev7.dist-info/RECORD +0 -111
  90. csv_detective-0.10.3.dev7.dist-info/WHEEL +0 -5
  91. csv_detective-0.10.3.dev7.dist-info/licenses/LICENSE +0 -21
  92. csv_detective-0.10.3.dev7.dist-info/top_level.txt +0 -3
  93. tests/__init__.py +0 -0
  94. tests/data/a_test_file.csv +0 -407
  95. tests/data/a_test_file.json +0 -394
  96. tests/data/b_test_file.csv +0 -7
  97. tests/data/c_test_file.csv +0 -2
  98. tests/data/csv_file +0 -7
  99. tests/data/file.csv.gz +0 -0
  100. tests/data/file.ods +0 -0
  101. tests/data/file.xls +0 -0
  102. tests/data/file.xlsx +0 -0
  103. tests/data/xlsx_file +0 -0
  104. tests/test_example.py +0 -67
  105. tests/test_fields.py +0 -175
  106. tests/test_file.py +0 -468
  107. tests/test_labels.py +0 -26
  108. tests/test_structure.py +0 -45
  109. tests/test_validation.py +0 -163
File without changes
File without changes
File without changes
File without changes
@@ -82,7 +82,22 @@ def detect_formats(
82
82
  # To reduce false positives: ensure these formats are detected only if the label yields
83
83
  # a detection (skipping the ones that have been excluded by the users).
84
84
  formats_with_mandatory_label = [
85
- f for f in fmtm.get_formats_with_mandatory_label() if f in scores_table.index
85
+ f
86
+ for f in [
87
+ "code_departement",
88
+ "code_commune_insee",
89
+ "code_postal",
90
+ "code_fantoir",
91
+ "latitude_wgs",
92
+ "longitude_wgs",
93
+ "latitude_wgs_fr_metropole",
94
+ "longitude_wgs_fr_metropole",
95
+ "latitude_l93",
96
+ "longitude_l93",
97
+ "siren",
98
+ "siret",
99
+ ]
100
+ if f in scores_table.index
86
101
  ]
87
102
  scores_table.loc[formats_with_mandatory_label, :] = np.where(
88
103
  scores_table_labels.loc[formats_with_mandatory_label, :],
@@ -91,16 +106,32 @@ def detect_formats(
91
106
  )
92
107
  analysis["columns"] = prepare_output_dict(scores_table, limited_output)
93
108
 
109
+ metier_to_python_type = {
110
+ "booleen": "bool",
111
+ "int": "int",
112
+ "float": "float",
113
+ "string": "string",
114
+ "json": "json",
115
+ "geojson": "json",
116
+ "datetime_aware": "datetime",
117
+ "datetime_naive": "datetime",
118
+ "datetime_rfc822": "datetime",
119
+ "date": "date",
120
+ "latitude_l93": "float",
121
+ "latitude_wgs": "float",
122
+ "latitude_wgs_fr_metropole": "float",
123
+ "longitude_l93": "float",
124
+ "longitude_wgs": "float",
125
+ "longitude_wgs_fr_metropole": "float",
126
+ "binary": "binary",
127
+ }
128
+
94
129
  if not limited_output:
95
130
  for detection_method in ["columns_fields", "columns_labels", "columns"]:
96
131
  analysis[detection_method] = {
97
132
  col_name: [
98
133
  {
99
- "python_type": (
100
- "string"
101
- if detection["format"] == "string"
102
- else fmtm.formats[detection["format"]].python_type
103
- ),
134
+ "python_type": metier_to_python_type.get(detection["format"], "string"),
104
135
  **detection,
105
136
  }
106
137
  for detection in detections
@@ -111,11 +142,7 @@ def detect_formats(
111
142
  for detection_method in ["columns_fields", "columns_labels", "columns"]:
112
143
  analysis[detection_method] = {
113
144
  col_name: {
114
- "python_type": (
115
- "string"
116
- if detection["format"] == "string"
117
- else fmtm.formats[detection["format"]].python_type
118
- ),
145
+ "python_type": metier_to_python_type.get(detection["format"], "string"),
119
146
  **detection,
120
147
  }
121
148
  for col_name, detection in analysis[detection_method].items()
@@ -5,22 +5,24 @@ from typing import TextIO
5
5
  from csv_detective.utils import display_logs_depending_process_time
6
6
 
7
7
 
8
- def detect_header_position(file: TextIO, verbose: bool = False) -> int:
8
+ def detect_headers(file: TextIO, sep: str, verbose: bool = False) -> tuple[int, list | None]:
9
9
  """Tests 10 first rows for possible header (in case header is not 1st row)"""
10
10
  if verbose:
11
11
  start = time()
12
- logging.info("Detecting header position")
12
+ logging.info("Detecting headers")
13
13
  file.seek(0)
14
14
  for i in range(10):
15
15
  row = file.readline()
16
16
  position = file.tell()
17
- next_row = file.readline()
18
- file.seek(position)
19
- if row != next_row:
20
- if verbose:
21
- display_logs_depending_process_time(
22
- f"Detected header position in {round(time() - start, 3)}s",
23
- time() - start,
24
- )
25
- return i
26
- raise ValueError("Could not accurately retrieve headers position")
17
+ headers = [c for c in row.replace("\n", "").split(sep) if c]
18
+ if not any(col == "" for col in headers):
19
+ next_row = file.readline()
20
+ file.seek(position)
21
+ if row != next_row:
22
+ if verbose:
23
+ display_logs_depending_process_time(
24
+ f"Detected headers in {round(time() - start, 3)}s",
25
+ time() - start,
26
+ )
27
+ return i, headers
28
+ raise ValueError("Could not retrieve headers")
@@ -2,7 +2,7 @@ import pandas as pd
2
2
 
3
3
 
4
4
  def remove_empty_first_rows(table: pd.DataFrame) -> tuple[pd.DataFrame, int]:
5
- """Analog process to detect_header_position for csv files, determines how many rows to skip
5
+ """Analog process to detect_headers for csv files, determines how many rows to skip
6
6
  to end up with the header at the right place"""
7
7
  idx = 0
8
8
  if all([str(c).startswith("Unnamed:") for c in table.columns]):
File without changes
File without changes
@@ -1,6 +1,5 @@
1
1
  import logging
2
2
  from time import time
3
- from typing import Iterator
4
3
 
5
4
  import pandas as pd
6
5
 
@@ -28,7 +27,7 @@ def routine(
28
27
  cast_json: bool = True,
29
28
  verbose: bool = False,
30
29
  sheet_name: str | int | None = None,
31
- ) -> dict | tuple[dict, Iterator[pd.DataFrame]]:
30
+ ) -> dict | tuple[dict, pd.DataFrame]:
32
31
  """
33
32
  Returns a dict with information about the table and possible column contents, and if requested the DataFrame with columns cast according to analysis.
34
33
 
@@ -116,7 +115,7 @@ def validate_then_detect(
116
115
  output_df: bool = False,
117
116
  cast_json: bool = True,
118
117
  verbose: bool = False,
119
- ) -> dict | tuple[dict, Iterator[pd.DataFrame]]:
118
+ ):
120
119
  """
121
120
  Performs a validation of the given file against the given analysis.
122
121
  If the validation fails, performs a full analysis and return it.
@@ -142,19 +141,20 @@ def validate_then_detect(
142
141
  if is_url(file_path):
143
142
  logging.info("Path recognized as a URL")
144
143
 
145
- is_valid, analysis, col_values = validate(
144
+ is_valid, table, analysis, col_values = validate(
146
145
  file_path=file_path,
147
146
  previous_analysis=previous_analysis,
148
147
  verbose=verbose,
149
148
  skipna=skipna,
150
149
  )
151
- if not is_valid:
152
- # if loading failed in validate, we load it from scratch and initiate an analysis
150
+ if analysis is None:
151
+ # if loading failed in validate, we load it from scratch
153
152
  table, analysis = load_file(
154
153
  file_path=file_path,
155
154
  num_rows=num_rows,
156
155
  verbose=verbose,
157
156
  )
157
+ if not is_valid:
158
158
  analysis, col_values = detect_formats(
159
159
  table=table,
160
160
  analysis=analysis,
@@ -164,18 +164,6 @@ def validate_then_detect(
164
164
  skipna=skipna,
165
165
  verbose=verbose,
166
166
  )
167
- else:
168
- # successful validation means we have a correct analysis and col_values
169
- # only need to reload the table, and we already know how
170
- table, _ = load_file(
171
- file_path=file_path,
172
- num_rows=num_rows,
173
- verbose=verbose,
174
- sep=analysis.get("separator"),
175
- encoding=analysis.get("encoding"),
176
- engine=analysis.get("engine"),
177
- sheet_name=analysis.get("sheet_name"),
178
- )
179
167
  try:
180
168
  return generate_output(
181
169
  table=table,
csv_detective/format.py CHANGED
@@ -9,11 +9,9 @@ class Format:
9
9
  name: str,
10
10
  func: Callable[[Any], bool],
11
11
  _test_values: dict[bool, list[str]],
12
- labels: dict[str, float] = {},
12
+ labels: list[str] = [],
13
13
  proportion: float = 1,
14
14
  tags: list[str] = [],
15
- mandatory_label: bool = False,
16
- python_type: str = "string",
17
15
  ) -> None:
18
16
  """
19
17
  Instanciates a Format object.
@@ -22,18 +20,16 @@ class Format:
22
20
  name: the name of the format.
23
21
  func: the value test for the format (returns whether a string is valid).
24
22
  _test_values: lists of valid and invalid values, used in the tests
25
- labels: the dict of hint headers and their credibilty for the header score (NB: credibility is relative witin a single format, should be used to rank the valid labels)
23
+ labels: the list of hint headers for the header score
26
24
  proportion: the tolerance (between 0 and 1) to say a column is valid for a format. (1 => 100% of the column has to pass the func check for the column to be considered valid)
27
25
  tags: to allow users to submit a file to only a subset of formats
28
26
  """
29
27
  self.name: str = name
30
- self.func: Callable[[Any], bool] = func
28
+ self.func: Callable = func
31
29
  self._test_values: dict[bool, list[str]] = _test_values
32
- self.labels: dict[str, float] = labels
30
+ self.labels: list[str] = labels
33
31
  self.proportion: float = proportion
34
32
  self.tags: list[str] = tags
35
- self.mandatory_label: bool = mandatory_label
36
- self.python_type: str = python_type
37
33
 
38
34
  def is_valid_label(self, val: str) -> float:
39
35
  return header_score(val, self.labels)
@@ -53,7 +49,7 @@ class FormatsManager:
53
49
  _test_values=module._test_values,
54
50
  **{
55
51
  attr: val
56
- for attr in ["labels", "proportion", "tags", "mandatory_label", "python_type"]
52
+ for attr in ["labels", "proportion", "tags"]
57
53
  if (val := getattr(module, attr, None))
58
54
  },
59
55
  )
@@ -67,8 +63,5 @@ class FormatsManager:
67
63
  if all(tag in fmt.tags for tag in tags)
68
64
  }
69
65
 
70
- def get_formats_with_mandatory_label(self) -> dict[str, Format]:
71
- return {label: fmt for label, fmt in self.formats.items() if fmt.mandatory_label}
72
-
73
66
  def available_tags(self) -> set[str]:
74
67
  return set(tag for format in self.formats.values() for tag in format.tags)
File without changes
@@ -2,15 +2,15 @@ from csv_detective.parsing.text import _process_text
2
2
 
3
3
  proportion = 0.55
4
4
  tags = ["fr", "geo"]
5
- labels = {
6
- "adresse": 1,
7
- "localisation": 1,
8
- "adresse postale": 1,
9
- "adresse geographique": 1,
10
- "adr": 0.5,
11
- "adresse complete": 1,
12
- "adresse station": 1,
13
- }
5
+ labels = [
6
+ "adresse",
7
+ "localisation",
8
+ "adresse postale",
9
+ "adresse geographique",
10
+ "adr",
11
+ "adresse complete",
12
+ "adresse station",
13
+ ]
14
14
 
15
15
  voies = {
16
16
  "aire ",
@@ -2,8 +2,7 @@ import codecs
2
2
 
3
3
  proportion = 1
4
4
  tags = ["type"]
5
- python_type = "binary"
6
- labels = {"bytes": 1, "binary": 1, "image": 1, "encode": 1, "content": 1}
5
+ labels = ["bytes", "binary", "image", "encode", "content"]
7
6
 
8
7
 
9
8
  def binary_casting(val: str) -> bytes:
@@ -1,7 +1,6 @@
1
1
  proportion = 1
2
2
  tags = ["type"]
3
- python_type = "bool"
4
- labels = {"is ": 1, "has ": 1, "est ": 1}
3
+ labels = ["is ", "has ", "est "]
5
4
 
6
5
  bool_mapping = {
7
6
  "1": True,
@@ -22,7 +21,7 @@ bool_mapping = {
22
21
  liste_bool = set(bool_mapping.keys())
23
22
 
24
23
 
25
- def bool_casting(val: str) -> bool | None:
24
+ def bool_casting(val: str) -> bool:
26
25
  return bool_mapping.get(val.lower())
27
26
 
28
27
 
@@ -2,18 +2,16 @@ from frformat import CodeCommuneInsee, Millesime
2
2
 
3
3
  proportion = 0.75
4
4
  tags = ["fr", "geo"]
5
- mandatory_label = True
6
- labels = {
7
- "code commune insee": 1,
8
- "code insee": 1,
9
- "codes insee": 1,
10
- "code commune": 1,
11
- "code insee commune": 1,
12
- "insee": 0.75,
13
- "code com": 1,
14
- "com": 0.5,
15
- "code": 0.5,
16
- }
5
+ labels = [
6
+ "code commune insee",
7
+ "code insee",
8
+ "codes insee",
9
+ "code commune",
10
+ "code insee commune",
11
+ "insee",
12
+ "code com",
13
+ "com",
14
+ ]
17
15
 
18
16
  _code_commune_insee = CodeCommuneInsee(Millesime.LATEST)
19
17
 
@@ -4,7 +4,7 @@ from csv_detective.parsing.text import _process_text
4
4
 
5
5
  proportion = 1
6
6
  tags = ["fr"]
7
- labels = {"code csp insee": 1, "code csp": 1}
7
+ labels = ["code csp insee", "code csp"]
8
8
 
9
9
 
10
10
  def _is(val):
@@ -2,14 +2,13 @@ from frformat import Millesime, NumeroDepartement, Options
2
2
 
3
3
  proportion = 1
4
4
  tags = ["fr", "geo"]
5
- mandatory_label = True
6
- labels = {
7
- "code departement": 1,
8
- "code_departement": 1,
9
- "dep": 0.5,
10
- "departement": 1,
11
- "dept": 0.75,
12
- }
5
+ labels = [
6
+ "code departement",
7
+ "code_departement",
8
+ "dep",
9
+ "departement",
10
+ "dept",
11
+ ]
13
12
 
14
13
  _options = Options(
15
14
  ignore_case=True,
@@ -2,12 +2,11 @@ from frformat import CodeFantoir
2
2
 
3
3
  proportion = 1
4
4
  tags = ["fr", "geo"]
5
- mandatory_label = True
6
- labels = {
7
- "cadastre1": 1,
8
- "code fantoir": 1,
9
- "fantoir": 1,
10
- }
5
+ labels = [
6
+ "cadastre1",
7
+ "code fantoir",
8
+ "fantoir",
9
+ ]
11
10
 
12
11
  _code_fantoir = CodeFantoir()
13
12
 
@@ -2,7 +2,7 @@ import re
2
2
 
3
3
  proportion = 0.9
4
4
  tags = ["fr"]
5
- labels = {"code": 0.5}
5
+ labels = ["code"]
6
6
 
7
7
  regex = r"^(\d{3}[SP]\d{4,10}(.\w{1,3}\d{0,5})?|\d[A-Z0-9]\d[SP]\w(\w-?\w{0,2}\d{0,6})?)$"
8
8
 
@@ -2,16 +2,15 @@ from frformat import CodePostal
2
2
 
3
3
  proportion = 0.9
4
4
  tags = ["fr", "geo"]
5
- mandatory_label = True
6
- labels = {
7
- "code postal": 1,
8
- "postal code": 1,
9
- "postcode": 1,
10
- "post code": 1,
11
- "cp": 0.5,
12
- "codes postaux": 1,
13
- "location postcode": 1,
14
- }
5
+ labels = [
6
+ "code postal",
7
+ "postal code",
8
+ "postcode",
9
+ "post code",
10
+ "cp",
11
+ "codes postaux",
12
+ "location postcode",
13
+ ]
15
14
 
16
15
  _code_postal = CodePostal()
17
16
 
@@ -2,13 +2,12 @@ from frformat import CodeRegion, Millesime
2
2
 
3
3
  proportion = 1
4
4
  tags = ["fr", "geo"]
5
- mandatory_label = True
6
- labels = {
7
- "code region": 1,
8
- "reg": 0.5,
9
- "code insee region": 1,
10
- "region": 1,
11
- }
5
+ labels = [
6
+ "code region",
7
+ "reg",
8
+ "code insee region",
9
+ "region",
10
+ ]
12
11
 
13
12
  _code_region = CodeRegion(Millesime.LATEST)
14
13
 
@@ -2,13 +2,12 @@ from frformat import CodeRNA
2
2
 
3
3
  proportion = 0.9
4
4
  tags = ["fr"]
5
- labels = {
6
- "code rna": 1,
7
- "rna": 1,
8
- "n° inscription association": 1,
9
- "identifiant association": 1,
10
- "asso": 0.75,
11
- }
5
+ labels = [
6
+ "code rna",
7
+ "rna",
8
+ "n° inscription association",
9
+ "identifiant association",
10
+ ]
12
11
 
13
12
  _code_rna = CodeRNA()
14
13
 
@@ -2,7 +2,7 @@ import re
2
2
 
3
3
  proportion = 0.9
4
4
  tags = ["fr"]
5
- labels = {"code waldec": 1, "waldec": 1}
5
+ labels = ["code waldec", "waldec"]
6
6
 
7
7
  regex = r"^W\d[\dA-Z]\d{7}$"
8
8
 
@@ -2,11 +2,11 @@ from frformat import Commune, Millesime, Options
2
2
 
3
3
  proportion = 0.8
4
4
  tags = ["fr", "geo"]
5
- labels = {
6
- "commune": 1,
7
- "ville": 1,
8
- "libelle commune": 1,
9
- }
5
+ labels = [
6
+ "commune",
7
+ "ville",
8
+ "libelle commune",
9
+ ]
10
10
 
11
11
  _options = Options(
12
12
  ignore_case=True,
@@ -4,12 +4,11 @@ from csv_detective.parsing.text import _process_text
4
4
 
5
5
  proportion = 1
6
6
  tags = ["fr"]
7
- labels = {
8
- "csp insee": 1,
9
- "csp": 0.75,
10
- "categorie socioprofessionnelle": 1,
11
- "sociopro": 1,
12
- }
7
+ labels = [
8
+ "csp insee",
9
+ "csp",
10
+ "categorie socioprofessionnelle",
11
+ ]
13
12
 
14
13
  f = open(join(dirname(__file__), "data", "csp_insee.txt"), "r")
15
14
  codes_insee = f.read().split("\n")
@@ -1,6 +1,6 @@
1
1
  0000Z
2
2
  0000Z
3
- 000Z
3
+ 000Z
4
4
  0111Z
5
5
  0112Z
6
6
  0113Z