csvsmith 0.4.0__tar.gz → 0.6.0__tar.gz
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.
- {csvsmith-0.4.0/src/csvsmith.egg-info → csvsmith-0.6.0}/PKG-INFO +45 -6
- {csvsmith-0.4.0 → csvsmith-0.6.0}/README.rst +44 -5
- {csvsmith-0.4.0 → csvsmith-0.6.0}/pyproject.toml +1 -1
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith/__init__.py +4 -1
- csvsmith-0.6.0/src/csvsmith/clean_numeric.py +124 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith/cli.py +32 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith/string_distance.py +7 -1
- {csvsmith-0.4.0 → csvsmith-0.6.0/src/csvsmith.egg-info}/PKG-INFO +45 -6
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith.egg-info/SOURCES.txt +2 -0
- csvsmith-0.6.0/tests/test_clean_numeric.py +36 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/tests/test_cli.py +31 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/LICENSE +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/setup.cfg +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith/classify.py +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith/excel2csv.py +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith/filter_rows.py +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith/move_files.py +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith/row_dedup.py +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith.egg-info/dependency_links.txt +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith.egg-info/entry_points.txt +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith.egg-info/requires.txt +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/src/csvsmith.egg-info/top_level.txt +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/tests/test_classify.py +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/tests/test_excel2csv.py +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/tests/test_filter_rows.py +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/tests/test_move_files.py +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/tests/test_row_dedup.py +0 -0
- {csvsmith-0.4.0 → csvsmith-0.6.0}/tests/test_string_distance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csvsmith
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Small CSV utilities: row deduplication, classification, row filtering, and CLI helpers.
|
|
5
5
|
Author-email: Eiichi YAMAMOTO <info@yeiichi.com>
|
|
6
6
|
License: MIT License
|
|
@@ -50,7 +50,7 @@ csvsmith
|
|
|
50
50
|
:target: https://pypi.org/project/csvsmith/
|
|
51
51
|
|
|
52
52
|
.. image:: https://img.shields.io/pypi/l/csvsmith.svg
|
|
53
|
-
:target: https://pypi.org/project/
|
|
53
|
+
:target: https://pypi.org/project/csvsmith/
|
|
54
54
|
|
|
55
55
|
Introduction
|
|
56
56
|
------------
|
|
@@ -77,6 +77,7 @@ Features
|
|
|
77
77
|
- rollback support via manifest
|
|
78
78
|
- row filtering by substring
|
|
79
79
|
- Excel worksheet to CSV conversion
|
|
80
|
+
- numeric string cleaning and normalization
|
|
80
81
|
- file moving by suffix
|
|
81
82
|
- string distance and similarity analysis
|
|
82
83
|
- a single command-line entrypoint with subcommands
|
|
@@ -153,7 +154,26 @@ Analyze string distance
|
|
|
153
154
|
print(result.jaro_winkler_score)
|
|
154
155
|
print(result.similarity_percentage)
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
Clean numeric values
|
|
158
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
159
|
+
|
|
160
|
+
.. code-block:: python
|
|
161
|
+
|
|
162
|
+
from csvsmith import clean_numeric
|
|
163
|
+
|
|
164
|
+
# Handles separators and parentheses for negative numbers
|
|
165
|
+
val1 = clean_numeric("1,234.56")
|
|
166
|
+
val2 = clean_numeric("(100.00)")
|
|
167
|
+
val3 = clean_numeric("1 234,56", sep=" ", decimal=",")
|
|
168
|
+
|
|
169
|
+
# Use relaxed mode to return original value on failure instead of raising ValueError
|
|
170
|
+
val4 = clean_numeric("not a number", relaxed=True)
|
|
171
|
+
|
|
172
|
+
print(val1) # 1234.56
|
|
173
|
+
print(val2) # -100.0
|
|
174
|
+
print(val3) # 1234.56
|
|
175
|
+
print(val4) # "not a number"
|
|
176
|
+
|
|
157
177
|
Drop rows in a CSV by column name
|
|
158
178
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
159
179
|
|
|
@@ -203,7 +223,7 @@ Move files by suffix
|
|
|
203
223
|
print(f"Moved {moved_count} files.")
|
|
204
224
|
|
|
205
225
|
CSV File Classification (Python)
|
|
206
|
-
|
|
226
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
207
227
|
|
|
208
228
|
.. code-block:: python
|
|
209
229
|
|
|
@@ -315,6 +335,25 @@ Move files by suffix
|
|
|
315
335
|
This moves files whose suffix matches one of the given values. The suffixes can
|
|
316
336
|
be written with or without a leading dot, and matching is case-insensitive.
|
|
317
337
|
|
|
338
|
+
Clean numeric values
|
|
339
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
340
|
+
|
|
341
|
+
.. code-block:: bash
|
|
342
|
+
|
|
343
|
+
csvsmith clean-numeric "1,234.56"
|
|
344
|
+
|
|
345
|
+
Using custom separators:
|
|
346
|
+
|
|
347
|
+
.. code-block:: bash
|
|
348
|
+
|
|
349
|
+
csvsmith clean-numeric "1 234,56" --sep " " --decimal ","
|
|
350
|
+
|
|
351
|
+
Relaxed mode (returns original string on failure):
|
|
352
|
+
|
|
353
|
+
.. code-block:: bash
|
|
354
|
+
|
|
355
|
+
csvsmith clean-numeric "not a number" --relaxed
|
|
356
|
+
|
|
318
357
|
Drop CSV rows
|
|
319
358
|
~~~~~~~~~~~~~
|
|
320
359
|
|
|
@@ -379,7 +418,7 @@ Example
|
|
|
379
418
|
|
|
380
419
|
Suppose you have a CSV like this:
|
|
381
420
|
|
|
382
|
-
.. code-block::
|
|
421
|
+
.. code-block:: text
|
|
383
422
|
|
|
384
423
|
id,name,notes
|
|
385
424
|
1,Alice,ok
|
|
@@ -394,7 +433,7 @@ Running:
|
|
|
394
433
|
|
|
395
434
|
produces a filtered file containing:
|
|
396
435
|
|
|
397
|
-
.. code-block::
|
|
436
|
+
.. code-block:: text
|
|
398
437
|
|
|
399
438
|
id,name,notes
|
|
400
439
|
1,Alice,ok
|
|
@@ -8,7 +8,7 @@ csvsmith
|
|
|
8
8
|
:target: https://pypi.org/project/csvsmith/
|
|
9
9
|
|
|
10
10
|
.. image:: https://img.shields.io/pypi/l/csvsmith.svg
|
|
11
|
-
:target: https://pypi.org/project/
|
|
11
|
+
:target: https://pypi.org/project/csvsmith/
|
|
12
12
|
|
|
13
13
|
Introduction
|
|
14
14
|
------------
|
|
@@ -35,6 +35,7 @@ Features
|
|
|
35
35
|
- rollback support via manifest
|
|
36
36
|
- row filtering by substring
|
|
37
37
|
- Excel worksheet to CSV conversion
|
|
38
|
+
- numeric string cleaning and normalization
|
|
38
39
|
- file moving by suffix
|
|
39
40
|
- string distance and similarity analysis
|
|
40
41
|
- a single command-line entrypoint with subcommands
|
|
@@ -111,7 +112,26 @@ Analyze string distance
|
|
|
111
112
|
print(result.jaro_winkler_score)
|
|
112
113
|
print(result.similarity_percentage)
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
Clean numeric values
|
|
116
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
117
|
+
|
|
118
|
+
.. code-block:: python
|
|
119
|
+
|
|
120
|
+
from csvsmith import clean_numeric
|
|
121
|
+
|
|
122
|
+
# Handles separators and parentheses for negative numbers
|
|
123
|
+
val1 = clean_numeric("1,234.56")
|
|
124
|
+
val2 = clean_numeric("(100.00)")
|
|
125
|
+
val3 = clean_numeric("1 234,56", sep=" ", decimal=",")
|
|
126
|
+
|
|
127
|
+
# Use relaxed mode to return original value on failure instead of raising ValueError
|
|
128
|
+
val4 = clean_numeric("not a number", relaxed=True)
|
|
129
|
+
|
|
130
|
+
print(val1) # 1234.56
|
|
131
|
+
print(val2) # -100.0
|
|
132
|
+
print(val3) # 1234.56
|
|
133
|
+
print(val4) # "not a number"
|
|
134
|
+
|
|
115
135
|
Drop rows in a CSV by column name
|
|
116
136
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
117
137
|
|
|
@@ -161,7 +181,7 @@ Move files by suffix
|
|
|
161
181
|
print(f"Moved {moved_count} files.")
|
|
162
182
|
|
|
163
183
|
CSV File Classification (Python)
|
|
164
|
-
|
|
184
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
165
185
|
|
|
166
186
|
.. code-block:: python
|
|
167
187
|
|
|
@@ -273,6 +293,25 @@ Move files by suffix
|
|
|
273
293
|
This moves files whose suffix matches one of the given values. The suffixes can
|
|
274
294
|
be written with or without a leading dot, and matching is case-insensitive.
|
|
275
295
|
|
|
296
|
+
Clean numeric values
|
|
297
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
298
|
+
|
|
299
|
+
.. code-block:: bash
|
|
300
|
+
|
|
301
|
+
csvsmith clean-numeric "1,234.56"
|
|
302
|
+
|
|
303
|
+
Using custom separators:
|
|
304
|
+
|
|
305
|
+
.. code-block:: bash
|
|
306
|
+
|
|
307
|
+
csvsmith clean-numeric "1 234,56" --sep " " --decimal ","
|
|
308
|
+
|
|
309
|
+
Relaxed mode (returns original string on failure):
|
|
310
|
+
|
|
311
|
+
.. code-block:: bash
|
|
312
|
+
|
|
313
|
+
csvsmith clean-numeric "not a number" --relaxed
|
|
314
|
+
|
|
276
315
|
Drop CSV rows
|
|
277
316
|
~~~~~~~~~~~~~
|
|
278
317
|
|
|
@@ -337,7 +376,7 @@ Example
|
|
|
337
376
|
|
|
338
377
|
Suppose you have a CSV like this:
|
|
339
378
|
|
|
340
|
-
.. code-block::
|
|
379
|
+
.. code-block:: text
|
|
341
380
|
|
|
342
381
|
id,name,notes
|
|
343
382
|
1,Alice,ok
|
|
@@ -352,7 +391,7 @@ Running:
|
|
|
352
391
|
|
|
353
392
|
produces a filtered file containing:
|
|
354
393
|
|
|
355
|
-
.. code-block::
|
|
394
|
+
.. code-block:: text
|
|
356
395
|
|
|
357
396
|
id,name,notes
|
|
358
397
|
1,Alice,ok
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "csvsmith"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Small CSV utilities: row deduplication, classification, row filtering, and CLI helpers."
|
|
9
9
|
readme = "README.rst"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -21,6 +21,7 @@ Compatibility aliases:
|
|
|
21
21
|
- CSVCleaner
|
|
22
22
|
|
|
23
23
|
Submodules:
|
|
24
|
+
- csvsmith.clean_numeric
|
|
24
25
|
- csvsmith.string_distance
|
|
25
26
|
- csvsmith.row_dedup
|
|
26
27
|
- csvsmith.classify
|
|
@@ -30,8 +31,9 @@ Submodules:
|
|
|
30
31
|
- csvsmith.cli (CLI entrypoint)
|
|
31
32
|
"""
|
|
32
33
|
|
|
33
|
-
__version__ = "0.
|
|
34
|
+
__version__ = "0.6.0"
|
|
34
35
|
|
|
36
|
+
from .clean_numeric import clean_numeric
|
|
35
37
|
from .row_dedup import (
|
|
36
38
|
count_duplicates_sorted,
|
|
37
39
|
add_row_digest,
|
|
@@ -61,4 +63,5 @@ __all__ = [
|
|
|
61
63
|
"Relation",
|
|
62
64
|
"Result",
|
|
63
65
|
"analyze_pair",
|
|
66
|
+
"clean_numeric",
|
|
64
67
|
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
NON_BREAKING_SPACE = "\xa0"
|
|
5
|
+
SEPARATOR_PATTERN = re.compile(r"[ _\xa0]")
|
|
6
|
+
NUMBER_PATTERN = re.compile(r"^-?(?:\d+|\d*\.\d+)$")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _normalize_numeric_text(value: Any, *, sep: str, decimal: str) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Normalize a numeric text string for consistent formatting.
|
|
12
|
+
|
|
13
|
+
Converts a given value to a string representation and ensures normalization of numeric formatting,
|
|
14
|
+
such as removing group separators, converting localized decimal separators, and handling negative
|
|
15
|
+
values enclosed in parentheses.
|
|
16
|
+
|
|
17
|
+
:param value: The value to be normalized, which may be of any type.
|
|
18
|
+
:type value: Any
|
|
19
|
+
:param sep: The character used as a group separator in the input value, which will be removed
|
|
20
|
+
during normalization.
|
|
21
|
+
:type sep: str
|
|
22
|
+
:param decimal: The character used as the decimal separator in the input value, which will
|
|
23
|
+
be replaced with a standard period ('.') during normalization.
|
|
24
|
+
:type decimal: str
|
|
25
|
+
:return: A normalized numeric string with consistent formatting.
|
|
26
|
+
:rtype: str
|
|
27
|
+
"""
|
|
28
|
+
numeric_text = str(value).strip()
|
|
29
|
+
|
|
30
|
+
if numeric_text.startswith("(") and numeric_text.endswith(")"):
|
|
31
|
+
numeric_text = f"-{numeric_text[1:-1]}"
|
|
32
|
+
|
|
33
|
+
if sep:
|
|
34
|
+
numeric_text = numeric_text.replace(sep, "")
|
|
35
|
+
|
|
36
|
+
if decimal != ".":
|
|
37
|
+
numeric_text = numeric_text.replace(decimal, ".")
|
|
38
|
+
|
|
39
|
+
return numeric_text
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _has_valid_grouping(numeric_text: str, *, decimal: str) -> bool:
|
|
43
|
+
"""
|
|
44
|
+
Checks whether a numeric text string has valid grouping based on a specified decimal character.
|
|
45
|
+
|
|
46
|
+
This function validates the structure of the given numeric text to determine if it adheres to allowed
|
|
47
|
+
grouping conventions. It ensures the string does not contain invalid or misplaced group separators,
|
|
48
|
+
decimal points, or spacing characters.
|
|
49
|
+
|
|
50
|
+
:param numeric_text: A string representing the numeric text to be validated.
|
|
51
|
+
:type numeric_text: str
|
|
52
|
+
:param decimal: A string representing the character used as the decimal point.
|
|
53
|
+
:type decimal: str
|
|
54
|
+
:return: True if the numeric text satisfies the grouping rules; otherwise, False.
|
|
55
|
+
:rtype: bool
|
|
56
|
+
"""
|
|
57
|
+
if not numeric_text:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
unsigned_text = numeric_text[1:] if numeric_text.startswith("-") else numeric_text
|
|
61
|
+
|
|
62
|
+
if unsigned_text.count(decimal) > 1:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
integer_text, _, fraction_text = unsigned_text.partition(decimal)
|
|
66
|
+
|
|
67
|
+
if not integer_text and not fraction_text:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
for part in (integer_text, fraction_text):
|
|
71
|
+
if not part:
|
|
72
|
+
continue
|
|
73
|
+
if part.startswith("_") or part.endswith("_"):
|
|
74
|
+
return False
|
|
75
|
+
if part.startswith(" ") or part.endswith(" "):
|
|
76
|
+
return False
|
|
77
|
+
if part.startswith(NON_BREAKING_SPACE) or part.endswith(NON_BREAKING_SPACE):
|
|
78
|
+
return False
|
|
79
|
+
if "__" in part or " " in part or NON_BREAKING_SPACE * 2 in part:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
stripped_text = SEPARATOR_PATTERN.sub("", numeric_text)
|
|
83
|
+
return bool(NUMBER_PATTERN.fullmatch(stripped_text))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def clean_numeric(
|
|
87
|
+
value: Any, *, sep: str = ",", decimal: str = ".", relaxed: bool = False
|
|
88
|
+
) -> float | Any:
|
|
89
|
+
"""
|
|
90
|
+
Cleans and converts a given input to a float by normalizing its numeric representation.
|
|
91
|
+
Handles separators and decimal points based on the provided arguments. If the input
|
|
92
|
+
value is invalid or cannot be converted, a ValueError is raised unless relaxed mode
|
|
93
|
+
is enabled.
|
|
94
|
+
|
|
95
|
+
:param value: The input value to be cleaned and converted.
|
|
96
|
+
:type value: Any
|
|
97
|
+
:param sep: The character used as a thousands separator in the input value. Default is ",".
|
|
98
|
+
:type sep: str
|
|
99
|
+
:param decimal: The character used as a decimal point in the input value. Default is ".".
|
|
100
|
+
:type decimal: str
|
|
101
|
+
:param relaxed: If True, return the original input when it is not numeric.
|
|
102
|
+
:type relaxed: bool
|
|
103
|
+
:return: The cleaned and converted numeric value as a float, or the original value in relaxed mode.
|
|
104
|
+
:rtype: float | Any
|
|
105
|
+
:raises ValueError: If the input value cannot be converted to a valid number and relaxed is False.
|
|
106
|
+
"""
|
|
107
|
+
if value is None:
|
|
108
|
+
return 0.0
|
|
109
|
+
|
|
110
|
+
normalized_number_text = _normalize_numeric_text(value, sep=sep, decimal=decimal)
|
|
111
|
+
|
|
112
|
+
if not _has_valid_grouping(normalized_number_text, decimal=decimal):
|
|
113
|
+
if relaxed:
|
|
114
|
+
return value
|
|
115
|
+
raise ValueError(f"Could not convert {value!r} to a valid number.")
|
|
116
|
+
|
|
117
|
+
numeric_text = SEPARATOR_PATTERN.sub("", normalized_number_text)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
return float(numeric_text)
|
|
121
|
+
except ValueError as exc:
|
|
122
|
+
if relaxed:
|
|
123
|
+
return value
|
|
124
|
+
raise ValueError(f"Could not convert {value!r} to a valid number.") from exc
|
|
@@ -9,6 +9,7 @@ from typing import Optional, Sequence
|
|
|
9
9
|
from . import __version__
|
|
10
10
|
from .classify import CSVClassifier
|
|
11
11
|
from .excel2csv import excel_to_csv
|
|
12
|
+
from .clean_numeric import clean_numeric
|
|
12
13
|
from .filter_rows import DropRowsBySubstring
|
|
13
14
|
from .move_files import move_by_suffix
|
|
14
15
|
from .row_dedup import (
|
|
@@ -148,6 +149,21 @@ def cmd_string_distance(args: argparse.Namespace) -> int:
|
|
|
148
149
|
return 0
|
|
149
150
|
|
|
150
151
|
|
|
152
|
+
def cmd_clean_numeric(args: argparse.Namespace) -> int:
|
|
153
|
+
try:
|
|
154
|
+
cleaned = clean_numeric(
|
|
155
|
+
args.value,
|
|
156
|
+
sep=args.sep,
|
|
157
|
+
decimal=args.decimal,
|
|
158
|
+
relaxed=args.relaxed,
|
|
159
|
+
)
|
|
160
|
+
print(cleaned)
|
|
161
|
+
except ValueError as e:
|
|
162
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
163
|
+
return 1
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
151
167
|
def _add_row_duplicates_parser(subparsers) -> None:
|
|
152
168
|
parser = subparsers.add_parser("row-duplicates", help="Find duplicate rows in a CSV.")
|
|
153
169
|
parser.add_argument("input", help="Input CSV file.")
|
|
@@ -230,6 +246,21 @@ def _add_string_distance_parser(subparsers) -> None:
|
|
|
230
246
|
parser.set_defaults(func=cmd_string_distance)
|
|
231
247
|
|
|
232
248
|
|
|
249
|
+
def _add_clean_numeric_parser(subparsers) -> None:
|
|
250
|
+
parser = subparsers.add_parser(
|
|
251
|
+
"clean-numeric", help="Clean and convert a numeric string to float."
|
|
252
|
+
)
|
|
253
|
+
parser.add_argument("value", help="Numeric value to clean.")
|
|
254
|
+
parser.add_argument("--sep", default=",", help="Group separator (default: ,).")
|
|
255
|
+
parser.add_argument("--decimal", default=".", help="Decimal separator (default: .).")
|
|
256
|
+
parser.add_argument(
|
|
257
|
+
"--relaxed",
|
|
258
|
+
action="store_true",
|
|
259
|
+
help="Return the original input when it is not numeric.",
|
|
260
|
+
)
|
|
261
|
+
parser.set_defaults(func=cmd_clean_numeric)
|
|
262
|
+
|
|
263
|
+
|
|
233
264
|
def build_parser() -> argparse.ArgumentParser:
|
|
234
265
|
parser = argparse.ArgumentParser(
|
|
235
266
|
prog="csvsmith",
|
|
@@ -249,6 +280,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
249
280
|
_add_excel_to_csv_parser(subparsers)
|
|
250
281
|
_add_drop_rows_parser(subparsers)
|
|
251
282
|
_add_string_distance_parser(subparsers)
|
|
283
|
+
_add_clean_numeric_parser(subparsers)
|
|
252
284
|
|
|
253
285
|
return parser
|
|
254
286
|
|
|
@@ -75,7 +75,13 @@ class StringDistance:
|
|
|
75
75
|
|
|
76
76
|
@staticmethod
|
|
77
77
|
def strip_all(s: str) -> str:
|
|
78
|
-
|
|
78
|
+
"""
|
|
79
|
+
Remove all whitespace characters from a string.
|
|
80
|
+
|
|
81
|
+
Uses split/join logic so any whitespace character acts as a separator,
|
|
82
|
+
including spaces, tabs, and newlines.
|
|
83
|
+
"""
|
|
84
|
+
return "".join(s.split())
|
|
79
85
|
|
|
80
86
|
@staticmethod
|
|
81
87
|
def equals_ignore_case(a: str, b: str) -> bool:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csvsmith
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Small CSV utilities: row deduplication, classification, row filtering, and CLI helpers.
|
|
5
5
|
Author-email: Eiichi YAMAMOTO <info@yeiichi.com>
|
|
6
6
|
License: MIT License
|
|
@@ -50,7 +50,7 @@ csvsmith
|
|
|
50
50
|
:target: https://pypi.org/project/csvsmith/
|
|
51
51
|
|
|
52
52
|
.. image:: https://img.shields.io/pypi/l/csvsmith.svg
|
|
53
|
-
:target: https://pypi.org/project/
|
|
53
|
+
:target: https://pypi.org/project/csvsmith/
|
|
54
54
|
|
|
55
55
|
Introduction
|
|
56
56
|
------------
|
|
@@ -77,6 +77,7 @@ Features
|
|
|
77
77
|
- rollback support via manifest
|
|
78
78
|
- row filtering by substring
|
|
79
79
|
- Excel worksheet to CSV conversion
|
|
80
|
+
- numeric string cleaning and normalization
|
|
80
81
|
- file moving by suffix
|
|
81
82
|
- string distance and similarity analysis
|
|
82
83
|
- a single command-line entrypoint with subcommands
|
|
@@ -153,7 +154,26 @@ Analyze string distance
|
|
|
153
154
|
print(result.jaro_winkler_score)
|
|
154
155
|
print(result.similarity_percentage)
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
Clean numeric values
|
|
158
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
159
|
+
|
|
160
|
+
.. code-block:: python
|
|
161
|
+
|
|
162
|
+
from csvsmith import clean_numeric
|
|
163
|
+
|
|
164
|
+
# Handles separators and parentheses for negative numbers
|
|
165
|
+
val1 = clean_numeric("1,234.56")
|
|
166
|
+
val2 = clean_numeric("(100.00)")
|
|
167
|
+
val3 = clean_numeric("1 234,56", sep=" ", decimal=",")
|
|
168
|
+
|
|
169
|
+
# Use relaxed mode to return original value on failure instead of raising ValueError
|
|
170
|
+
val4 = clean_numeric("not a number", relaxed=True)
|
|
171
|
+
|
|
172
|
+
print(val1) # 1234.56
|
|
173
|
+
print(val2) # -100.0
|
|
174
|
+
print(val3) # 1234.56
|
|
175
|
+
print(val4) # "not a number"
|
|
176
|
+
|
|
157
177
|
Drop rows in a CSV by column name
|
|
158
178
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
159
179
|
|
|
@@ -203,7 +223,7 @@ Move files by suffix
|
|
|
203
223
|
print(f"Moved {moved_count} files.")
|
|
204
224
|
|
|
205
225
|
CSV File Classification (Python)
|
|
206
|
-
|
|
226
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
207
227
|
|
|
208
228
|
.. code-block:: python
|
|
209
229
|
|
|
@@ -315,6 +335,25 @@ Move files by suffix
|
|
|
315
335
|
This moves files whose suffix matches one of the given values. The suffixes can
|
|
316
336
|
be written with or without a leading dot, and matching is case-insensitive.
|
|
317
337
|
|
|
338
|
+
Clean numeric values
|
|
339
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
340
|
+
|
|
341
|
+
.. code-block:: bash
|
|
342
|
+
|
|
343
|
+
csvsmith clean-numeric "1,234.56"
|
|
344
|
+
|
|
345
|
+
Using custom separators:
|
|
346
|
+
|
|
347
|
+
.. code-block:: bash
|
|
348
|
+
|
|
349
|
+
csvsmith clean-numeric "1 234,56" --sep " " --decimal ","
|
|
350
|
+
|
|
351
|
+
Relaxed mode (returns original string on failure):
|
|
352
|
+
|
|
353
|
+
.. code-block:: bash
|
|
354
|
+
|
|
355
|
+
csvsmith clean-numeric "not a number" --relaxed
|
|
356
|
+
|
|
318
357
|
Drop CSV rows
|
|
319
358
|
~~~~~~~~~~~~~
|
|
320
359
|
|
|
@@ -379,7 +418,7 @@ Example
|
|
|
379
418
|
|
|
380
419
|
Suppose you have a CSV like this:
|
|
381
420
|
|
|
382
|
-
.. code-block::
|
|
421
|
+
.. code-block:: text
|
|
383
422
|
|
|
384
423
|
id,name,notes
|
|
385
424
|
1,Alice,ok
|
|
@@ -394,7 +433,7 @@ Running:
|
|
|
394
433
|
|
|
395
434
|
produces a filtered file containing:
|
|
396
435
|
|
|
397
|
-
.. code-block::
|
|
436
|
+
.. code-block:: text
|
|
398
437
|
|
|
399
438
|
id,name,notes
|
|
400
439
|
1,Alice,ok
|
|
@@ -3,6 +3,7 @@ README.rst
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/csvsmith/__init__.py
|
|
5
5
|
src/csvsmith/classify.py
|
|
6
|
+
src/csvsmith/clean_numeric.py
|
|
6
7
|
src/csvsmith/cli.py
|
|
7
8
|
src/csvsmith/excel2csv.py
|
|
8
9
|
src/csvsmith/filter_rows.py
|
|
@@ -16,6 +17,7 @@ src/csvsmith.egg-info/entry_points.txt
|
|
|
16
17
|
src/csvsmith.egg-info/requires.txt
|
|
17
18
|
src/csvsmith.egg-info/top_level.txt
|
|
18
19
|
tests/test_classify.py
|
|
20
|
+
tests/test_clean_numeric.py
|
|
19
21
|
tests/test_cli.py
|
|
20
22
|
tests/test_excel2csv.py
|
|
21
23
|
tests/test_filter_rows.py
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from src.csvsmith.clean_numeric import clean_numeric
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_clean_numeric_with_valid_integer_string() -> None:
|
|
6
|
+
assert clean_numeric("1,234") == 1234.0
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_clean_numeric_with_valid_float_string() -> None:
|
|
10
|
+
assert clean_numeric("1,234.56") == 1234.56
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_clean_numeric_with_custom_separator() -> None:
|
|
14
|
+
assert clean_numeric("1.234,56", sep=".", decimal=",") == 1234.56
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_clean_numeric_with_negative_value() -> None:
|
|
18
|
+
assert clean_numeric("-1,234.56") == -1234.56
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_clean_numeric_with_parenthesis_for_negative() -> None:
|
|
22
|
+
assert clean_numeric("(1,234.56)") == -1234.56
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_clean_numeric_with_invalid_value_raises_valueerror() -> None:
|
|
26
|
+
with pytest.raises(ValueError, match="Could not convert 'abc'.*"):
|
|
27
|
+
clean_numeric("abc")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_clean_numeric_with_multiple_decimal_points_raises_valueerror() -> None:
|
|
31
|
+
with pytest.raises(ValueError, match="Could not convert '1,234.5.6'.*"):
|
|
32
|
+
clean_numeric("1,234.5.6")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_clean_numeric_with_none_value() -> None:
|
|
36
|
+
assert clean_numeric(None) == 0.0
|
|
@@ -126,3 +126,34 @@ def test_cli_parses_move_files_command_with_suffixes_without_dots():
|
|
|
126
126
|
assert args.source == "src_dir"
|
|
127
127
|
assert args.dest == "dst_dir"
|
|
128
128
|
assert args.suffixes == "csv,pdf"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_cli_parses_clean_numeric_command():
|
|
132
|
+
parser = build_parser()
|
|
133
|
+
args = parser.parse_args(["clean-numeric", "1,234.56", "--sep", ",", "--decimal", "."])
|
|
134
|
+
|
|
135
|
+
assert args.command == "clean-numeric"
|
|
136
|
+
assert args.value == "1,234.56"
|
|
137
|
+
assert args.sep == ","
|
|
138
|
+
assert args.decimal == "."
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_cli_parses_clean_numeric_command_with_relaxed_mode():
|
|
142
|
+
parser = build_parser()
|
|
143
|
+
args = parser.parse_args(
|
|
144
|
+
["clean-numeric", "not-a-number", "--relaxed"]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
assert args.command == "clean-numeric"
|
|
148
|
+
assert args.value == "not-a-number"
|
|
149
|
+
assert args.relaxed is True
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_cli_parses_string_distance_command():
|
|
153
|
+
parser = build_parser()
|
|
154
|
+
args = parser.parse_args(["string-distance", "a", "b", "--ignore-case"])
|
|
155
|
+
|
|
156
|
+
assert args.command == "string-distance"
|
|
157
|
+
assert args.string_a == "a"
|
|
158
|
+
assert args.string_b == "b"
|
|
159
|
+
assert args.ignore_case is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|