-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
executable file
·237 lines (208 loc) · 9.2 KB
/
Copy pathmain.py
File metadata and controls
executable file
·237 lines (208 loc) · 9.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
#!/bin/env python3
import argparse
import bisect
import os
import re
import sys
from collections.abc import Callable
from functools import cmp_to_key
from typing import Any
class InteractiveSort:
def __init__(self, args: argparse.Namespace) -> None:
self.data: list[str] = []
self.args: argparse.Namespace = args
self.month_map: dict[str, int] = {
"JAN": 1,
"FEB": 2,
"MAR": 3,
"APR": 4,
"MAY": 5,
"JUN": 6,
"JUL": 7,
"AUG": 8,
"SEP": 9,
"OCT": 10,
"NOV": 11,
"DEC": 12,
}
def get_sort_key(self, item: str) -> str:
"""Generates a key for sorting based on the provided arguments."""
key: str = item
if self.args.field_separator:
fields: list[str] = key.split(self.args.field_separator)
if fields:
key = fields[0]
else:
return "" # Empty if no fields after splitting
if self.args.ignore_case:
key = key.lower()
if self.args.ignore_nonprinting:
key = "".join(filter(str.isprintable, key))
return key
def month_sort_key(self, item: str) -> int:
"""Extracts and converts the month from the beginning of the string (or first field)."""
text_to_check: str = item
if self.args.field_separator:
fields: list[str] = item.split(self.args.field_separator)
if fields:
text_to_check = fields[0]
else:
return 0
match: re.Match[str] | None = re.match(r"^(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)\s*", text_to_check, re.IGNORECASE)
if match:
return self.month_map.get(match.group(1).upper(), 0) # 0 for unknown
return 0 # Treat lines without a month at the beginning as "unknown"
def human_numeric_sort_key(self, item: str) -> float | str:
"""Converts a human-readable number string to a comparable tuple (or from the first field)."""
text_to_check: str = item
if self.args.field_separator:
fields: list[str] = item.split(self.args.field_separator)
if fields:
text_to_check = fields[0]
else:
return item # Fallback
parts: list[tuple[str, str]] = re.findall(r"(\d+\.?\d*)([KMGTPEZY]?)", text_to_check, re.IGNORECASE)
if parts:
value_str: str
unit: str
value_str, unit = parts[0]
value: float = float(value_str)
unit = unit.upper()
if unit == "K":
value *= 1024
elif unit == "M":
value *= 1024**2
elif unit == "G":
value *= 1024**3
elif unit == "T":
value *= 1024**4
elif unit == "P":
value *= 1024**5
elif unit == "E":
value *= 1024**6
elif unit == "Z":
value *= 1024**7
elif unit == "Y":
value *= 1024**8
return value
try:
return float(text_to_check) # Fallback to numeric sort if no human-readable number
except ValueError:
return text_to_check # Keep original string if not convertible to number
def numeric_sort_key(self, item: str) -> float | str:
"""Tries to convert the item (or first field) to a float for numeric sorting."""
text_to_check: str = item
if self.args.field_separator:
fields: list[str] = item.split(self.args.field_separator)
if fields:
text_to_check = fields[0]
else:
return item # Fallback
try:
return float(text_to_check)
except ValueError:
return text_to_check # Keep original string if not a valid number
def compare_items(self, item1: str, item2: str) -> int:
"""Compares two items based on the active sorting options."""
if self.args.month_sort:
key1: int = self.month_sort_key(item1)
key2: int = self.month_sort_key(item2)
if key1 != key2:
return key1 - key2
if self.args.human_numeric_sort:
key1_human: float | str = self.human_numeric_sort_key(item1)
key2_human: float | str = self.human_numeric_sort_key(item2)
if isinstance(key1_human, int | float) and isinstance(key2_human, int | float):
if key1_human != key2_human:
return int(key1_human - key2_human)
if self.args.numeric_sort:
key1_numeric: float | str = self.numeric_sort_key(item1)
key2_numeric: float | str = self.numeric_sort_key(item2)
if isinstance(key1_numeric, int | float) and isinstance(key2_numeric, int | float):
if key1_numeric != key2_numeric:
return int(key1_numeric - key2_numeric)
# Default string comparison (after applying ignore-case and ignore-nonprinting)
key1_str: str = self.get_sort_key(item1)
key2_str: str = self.get_sort_key(item2)
if key1_str < key2_str:
return -1
elif key1_str > key2_str:
return 1
else:
return 0
def add_item(self, item: str) -> None:
"""Adds an item to the sorted data using a custom comparison."""
if not self.args.month_sort and not self.args.human_numeric_sort and not self.args.numeric_sort:
key_func: Callable[[str], Any] = self.get_sort_key
else:
key_func = cmp_to_key(self.compare_items)
bisect.insort_left(self.data, item, key=key_func)
def display_sorted(self) -> None:
"""Displays the current sorted data."""
output_stream = sys.stderr if self.args.stderr else sys.stdout
if not self.args.stderr:
os.system("cls" if os.name == "nt" else "clear")
print("--- Interactive Sorted Data ---", file=output_stream)
items_to_display: list[str] = self.data
if self.args.unique:
items_to_display = sorted(
set(items_to_display), key=lambda x: self.data.index(x)
) # Maintain original order of first occurrence
if self.args.reverse:
items_to_display = list(reversed(items_to_display))
for item in items_to_display:
print(item, file=output_stream)
print("-----------------------------", file=output_stream)
def output_final_sorted(self) -> None:
"""Outputs the final sorted data to stdout (used when stderr flag is set)."""
items_to_display: list[str] = self.data
if self.args.unique:
items_to_display = sorted(
set(items_to_display), key=lambda x: self.data.index(x)
) # Maintain original order of first occurrence
if self.args.reverse:
items_to_display = list(reversed(items_to_display))
for item in items_to_display:
print(item, file=sys.stdout)
def run(self) -> None:
"""Runs the interactive sort application."""
output_stream = sys.stderr if self.args.stderr else sys.stdout
print("Enter lines of text. Press Ctrl+D (or Ctrl+Z then Enter on Windows) to finish.", file=output_stream)
try:
while True:
line: str = input()
self.add_item(line)
self.display_sorted()
except EOFError:
if self.args.stderr:
# When using stderr flag, output final sorted result to stdout
self.output_final_sorted()
else:
print("\nEnd of input.", file=output_stream)
except KeyboardInterrupt:
print("\nExiting.", file=output_stream)
def main() -> None:
"""Main entry point for the interactive-sort command."""
parser: argparse.ArgumentParser = argparse.ArgumentParser(description="Interactively sort input lines.")
parser.add_argument("-f", "--ignore-case", action="store_true", help="fold lower case to upper case characters")
parser.add_argument("-i", "--ignore-nonprinting", action="store_true", help="consider only printable characters")
parser.add_argument("-M", "--month-sort", action="store_true", help="compare (unknown) < 'JAN' < ... < 'DEC'")
parser.add_argument(
"-H", "--human-numeric-sort", action="store_true", help="compare human readable numbers (e.g., 2K 1G)"
)
parser.add_argument("-n", "--numeric-sort", action="store_true", help="compare according to string numerical value")
parser.add_argument("-r", "--reverse", action="store_true", help="reverse the result of comparisons")
parser.add_argument("-u", "--unique", action="store_true", help="output only the first of an equal sequence")
parser.add_argument(
"-t",
"--field-separator",
type=str,
default=None,
help="use FIELD as the field separator when finding an ordering key",
)
parser.add_argument("--stderr", action="store_true", help="output to stderr for debugging")
args: argparse.Namespace = parser.parse_args()
sorter: InteractiveSort = InteractiveSort(args)
sorter.run()
if __name__ == "__main__":
main()