feasytools 0.0.1__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.
feasytools/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ from .tfunc import *
2
+ from .argchk import *
3
+ from .table import *
4
+ from .pq import *
5
+ from .rangelist import *
6
+
7
+ def time2str(tspan:float):
8
+ tspan=round(tspan)
9
+ s=tspan%60
10
+ m=tspan//60%60
11
+ h=tspan//3600
12
+ return f"{h:02}:{m:02}:{s:02}"
feasytools/argchk.py ADDED
@@ -0,0 +1,67 @@
1
+ # 具有类型注释功能的ArgParse: ArgChecker
2
+ import sys
3
+ from typing import Any, Optional, Union
4
+
5
+ class ArgChecker:
6
+ @staticmethod
7
+ def __cast(v:str)->'Union[None,bool,int,float,str]':
8
+ if v=="True": return True
9
+ if v=="False": return False
10
+ if v=="None": return None
11
+ try:
12
+ return int(v)
13
+ except:
14
+ pass
15
+ try:
16
+ return float(v)
17
+ except:
18
+ return v.strip('"')
19
+ @staticmethod
20
+ def get_dict()->'dict[str,Union[str,Any]]':
21
+ '''将输入参数以字典的形式返回'''
22
+ cur_key=None
23
+ ret={}
24
+ for v in sys.argv[1:]:
25
+ if v.startswith('-'):
26
+ if cur_key!=None:
27
+ ret[cur_key]=True
28
+ cur_key=v.strip('-')
29
+ elif cur_key!=None:
30
+ ret[cur_key]=ArgChecker.__cast(v)
31
+ cur_key=None
32
+ else:
33
+ raise ValueError(f"无效参数'{v}'")
34
+ if cur_key!=None: ret[cur_key]=True
35
+ return ret
36
+
37
+ def __init__(self,pars: 'Optional[dict[str,Any]]'=None):
38
+ self.__args=self.get_dict() if pars is None else pars
39
+
40
+ def pop_bool(self, key: str) -> bool:
41
+ if self.__args.pop(key, False): return True
42
+ return False
43
+
44
+ def pop_int(self, key: str, default: Optional[int] = None) -> int:
45
+ val = self.__args.pop(key, default)
46
+ if val is None: raise ValueError(f"必须指定'{key}'参数")
47
+ return int(val)
48
+
49
+ def pop_str(self, key: str, default: Optional[str]=None) -> str:
50
+ val = self.__args.pop(key, default)
51
+ if val is None: raise ValueError(f"必须指定'{key}'参数")
52
+ return str(val)
53
+
54
+ def pop_float(self, key: str, default: Optional[float] = None) -> float:
55
+ val = self.__args.pop(key, default)
56
+ if val is None: raise ValueError(f"必须指定'{key}'参数")
57
+ return float(val)
58
+
59
+ def empty(self) -> bool:
60
+ return len(self.__args) == 0
61
+
62
+ def keys(self): return self.__args.keys()
63
+ def values(self): return self.__args.values()
64
+ def items(self): return self.__args.items()
65
+
66
+ def __str__(self):
67
+ return str(self.__args)
feasytools/pq.py ADDED
@@ -0,0 +1,185 @@
1
+ import heapq
2
+ from collections import deque
3
+ from typing import Generic, TypeVar
4
+
5
+ QItem=TypeVar("QItem")
6
+
7
+ class Heap(Generic[QItem]):
8
+ '''小根堆数据结构'''
9
+ def __init__(self):
10
+ self._q=[]
11
+
12
+ def push(self,item:QItem):
13
+ '''入堆, O(log n)'''
14
+ heapq.heappush(self._q,item)
15
+
16
+ def pop(self)->QItem:
17
+ '''弹出堆顶元素, O(log n)'''
18
+ return heapq.heappop(self._q)
19
+
20
+ def remove(self,item)->bool:
21
+ '''弹出item元素, O(n)'''
22
+ try:
23
+ idx=self._q.index(item)
24
+ except ValueError:
25
+ return False
26
+ self._q.pop(idx)
27
+ heapq.heapify(self._q)
28
+ return True
29
+
30
+ @property
31
+ def top(self)->QItem:
32
+ '''获取堆顶元素,但不弹出, O(1)'''
33
+ return self._q[0]
34
+
35
+ def __len__(self)->int:
36
+ return len(self._q)
37
+
38
+ def __contains__(self,obj)->bool:
39
+ '''检查元素是否存在, O(n)'''
40
+ return obj in self._q
41
+
42
+ def empty(self)->bool:
43
+ '''检测堆是否为空, O(1)'''
44
+ return len(self._q)==0
45
+
46
+ class PQueue(Generic[QItem]):
47
+ '''优先级队列(小根堆数据结构)'''
48
+ def __init__(self):
49
+ self._q:'list[tuple[int,QItem]]'=[]
50
+
51
+ def push(self,pri:int,item:QItem)->None:
52
+ '''
53
+ 入队, O(log n)
54
+ pri: 优先级
55
+ item: 元素
56
+ '''
57
+ heapq.heappush(self._q,(pri,item))
58
+
59
+ def pop(self)->'tuple[int,QItem]':
60
+ '''获取队首元素并出队, O(log n)'''
61
+ return heapq.heappop(self._q)
62
+
63
+ def remove(self,item:QItem)->bool:
64
+ '''弹出第一个item元素, O(n), 返回是否成功'''
65
+ idx=-1
66
+ for i,(_,data) in enumerate(self._q):
67
+ if data == item:
68
+ idx = i
69
+ break
70
+ if idx==-1: return False
71
+ self._q.pop(idx)
72
+ heapq.heapify(self._q)
73
+ return True
74
+
75
+ @property
76
+ def top(self)->'tuple[int,QItem]':
77
+ '''获取队首元素,但不出队, O(1)'''
78
+ return self._q[0]
79
+
80
+ def __len__(self)->int:
81
+ return len(self._q)
82
+
83
+ def __contains__(self,obj)->bool:
84
+ return obj in self._q
85
+
86
+ def empty(self)->bool:
87
+ '''检测队列是否为空, O(1)'''
88
+ return len(self._q)==0
89
+
90
+ def __str__(self)->str:
91
+ '''转为字符串, O(nlogn)'''
92
+ q2=self._q.copy()
93
+ q2.sort()
94
+ return str(q2)
95
+
96
+ class BufferedPQ(Generic[QItem]):
97
+ '''
98
+ 含有缓冲的优先级队列。
99
+ 该数据结构分为两部分,一个普通队列(记为q)和一个优先级队列(记为p)。
100
+ 优先级队列大小恒定(设大小为n),普通队列大小可变(设大小为m)。
101
+ 数据进入普通队列,而后进入优先级队列,最后离开本结构。
102
+ '''
103
+ def __init__(self,p_size:int):
104
+ self._sz=p_size
105
+ self._P:'PQueue[QItem]'=PQueue()
106
+ self._Q:'deque[tuple[int,QItem]]'=deque()
107
+ self._sP=set()
108
+ self._sQ=set()
109
+
110
+ def __len__(self)->int:
111
+ return len(self._P)+len(self._Q)
112
+
113
+ @property
114
+ def p_size(self)->int:
115
+ '''获取优先级区大小'''
116
+ return self._sz
117
+
118
+ @property
119
+ def p_len(self)->int:
120
+ '''检查优先级区长度, O(1)'''
121
+ return len(self._P)
122
+
123
+ @property
124
+ def q_len(self)->int:
125
+ '''检查缓冲区长度, O(1)'''
126
+ return len(self._Q)
127
+
128
+ def __contains__(self,obj)->bool:
129
+ '''检查元素是否存在, O(1)'''
130
+ return obj in self._sP or obj in self._sQ
131
+
132
+ def push(self,pri:int,obj:QItem)->bool:
133
+ '''
134
+ 入队, O(log n)
135
+ pri: 优先级
136
+ item: 元素
137
+ '''
138
+ if self.__contains__(obj): return False
139
+ if len(self._P)<self._sz:
140
+ self._P.push(pri,obj)
141
+ self._sP.add(obj)
142
+ else:
143
+ self._Q.append((pri,obj))
144
+ self._sQ.add(obj)
145
+ return True
146
+
147
+ def top(self)->'tuple[int,QItem]':
148
+ '''获取队首元素,但不出队, O(1)'''
149
+ return self._P.top
150
+
151
+ def pop(self)->'tuple[int,QItem]':
152
+ '''获取队首元素并出队, O(log n)'''
153
+ ret=self._P.pop()
154
+ self._sP.remove(ret[1])
155
+ if len(self._Q)>0:
156
+ pri,obj=self._Q.popleft()
157
+ self._sQ.remove(obj)
158
+ self._P.push(pri,obj)
159
+ self._sP.add(obj)
160
+ return ret
161
+
162
+ def empty(self)->bool:
163
+ '''检测队列是否为空, O(1)'''
164
+ return self._P.empty()
165
+
166
+ def remove(self,obj)->bool:
167
+ '''弹出第一个obj元素, O(n+m), 返回是否成功'''
168
+ if self._P.remove(obj): return True
169
+ try:
170
+ self._Q.remove(obj)
171
+ return True
172
+ except:
173
+ return False
174
+
175
+ def p_has(self,obj)->bool:
176
+ '''检查元素是否存在于优先级区, O(1)'''
177
+ return obj in self._sP
178
+
179
+ def q_has(self,obj)->bool:
180
+ '''检查元素是否存在于缓冲区, O(1)'''
181
+ return obj in self._sQ
182
+
183
+ def __str__(self)->str:
184
+ '''转为字符串, O(nlogn+m)'''
185
+ return f"BufferedPQ[P={self._P},Q={self._Q}]"
@@ -0,0 +1,46 @@
1
+ import xml.etree.ElementTree as ET
2
+ from typing import Union, overload
3
+
4
+ class RangeList:
5
+ '''时间范围列表,用于表示一系列时间段'''
6
+ @staticmethod
7
+ def parse_time(s:str)->int:
8
+ '''将时间字符串转换为秒数, 支持格式为hh:mm:ss或者一个可转化成int的字符串'''
9
+ try:
10
+ return int(s)
11
+ except:
12
+ h,m,s = s.split(":")
13
+ return int(h)*3600+int(m)*60+int(s)
14
+
15
+ @overload
16
+ def __init__(self, data: ET.Element): '''从xml节点初始化'''
17
+ @overload
18
+ def __init__(self, data: 'list[tuple[int,int]]'): '''从列表初始化'''
19
+
20
+ def __init__(self, data: 'Union[list[tuple[int,int]], ET.Element]'):
21
+ if isinstance(data,ET.Element):
22
+ loop_period = int(data.attrib.get("loop_period", 0))
23
+ loop_times = int(data.attrib.get("loop_times", 1))
24
+ assert loop_times >= 1
25
+ data = [(self.parse_time(itm.attrib['btime']),self.parse_time(itm.attrib["etime"])) for itm in data]
26
+ if loop_times > 1 and len(data) > 1:
27
+ assert loop_period > data[-1][1]
28
+ n = len(data)
29
+ for j in range(1, loop_times):
30
+ for i in range(n):
31
+ if data[i][0]>=data[i][1]: raise ValueError(f"起始时间{data[i][0]}晚于终止时间{data[i][1]}")
32
+ data.append((data[i][0]+loop_period*j,data[i][1]+loop_period*j))
33
+ self._d:'list[tuple[int,int]]' = data
34
+
35
+ def __contains__(self, t:int):
36
+ for (l,r) in self._d:
37
+ if l<=t and t<r: return True
38
+ return False
39
+
40
+ def __len__(self)->int: return self._d.__len__()
41
+
42
+ def __getitem__(self, indices): return self._d.__getitem__(indices)
43
+
44
+ def __str__(self): return str(self._d)
45
+
46
+ def __iter__(self): return iter(self._d)
feasytools/table.py ADDED
@@ -0,0 +1,490 @@
1
+ import struct, gzip
2
+ from typing import IO, BinaryIO, Generic, Optional, TextIO, Type, TypeVar, Union, Iterable, overload
3
+ import numpy as np
4
+ from abc import abstractmethod, ABCMeta
5
+
6
+ Table_DType = Type[Union[np.int32,np.float32]]
7
+ _Table_DType = TypeVar("_Table_DType",np.int32,np.float32)
8
+
9
+ def _Lchk(L):
10
+ assert L=='i' or L=='f'
11
+
12
+ def _dtypechk(dtype):
13
+ assert dtype==np.int32 or dtype==np.float32
14
+
15
+ def _L2dtype(L)->Type:
16
+ return np.int32 if L=='i' else np.float32
17
+
18
+ def _dtype2L(dtype)->str:
19
+ return 'i' if dtype==np.int32 else 'f'
20
+
21
+ class TableWriter(metaclass = ABCMeta):
22
+ '''数据表写入器(抽象类)'''
23
+ @abstractmethod
24
+ def __init__(self,col_names:'list[str]',dtype:Type)->None:
25
+ _dtypechk(dtype)
26
+ self._col_names:'list[str]' = col_names
27
+ self._dtype:Table_DType = dtype
28
+ @property
29
+ def col_num(self)->int:
30
+ '''数据表的列数'''
31
+ return len(self._col_names)
32
+ @property
33
+ def dtype(self)->Type:
34
+ '''数据表的数据类型'''
35
+ return self._dtype
36
+ @abstractmethod
37
+ def write(self,data:list)->None: '''写入一条数据, 数据长度必须与列数相等'''
38
+ @abstractmethod
39
+ def write_all(self,data:np.ndarray)->None: '''写入多条数据, 数据长度(参数ndarray的列数)必须与本表的列数相等'''
40
+ @abstractmethod
41
+ def close(self)->None: '''关闭写入器'''
42
+
43
+ class FileTableWriter(TableWriter):
44
+ '''文件数据表写入器(抽象类)'''
45
+ @abstractmethod
46
+ def __init__(self,fh:IO,col_names:'list[str]',dtype:Type)->None:
47
+ super().__init__(col_names,dtype)
48
+ self._fh=fh
49
+
50
+ class MemoryTableWriter(TableWriter):
51
+ '''内存数据表写入器'''
52
+ _data:np.ndarray
53
+ def __init__(self,col_names:'list[str]',dtype:Type)->None:
54
+ '''
55
+ 初始化
56
+ col_names: 列名
57
+ dtype: 数据类型, 可以是np.int32或np.float32
58
+ '''
59
+ super().__init__(col_names,dtype)
60
+ self._data=np.zeros((0,len(col_names)),dtype=dtype)
61
+
62
+ def write(self,data:list)->None:
63
+ self._data=np.vstack([self._data,np.array(data,dtype=self._dtype)])
64
+
65
+ def write_all(self,data:np.ndarray)->None:
66
+ self._data=np.vstack([self._data,data])
67
+
68
+ def close(self): pass
69
+
70
+ @property
71
+ def data(self)->np.ndarray:
72
+ '''获取写入的所有数据'''
73
+ return self._data
74
+
75
+ class CsvTableWriter(FileTableWriter):
76
+ '''CSV数据表写入器'''
77
+ _fh: TextIO
78
+ def __init__(self,fname:str,col_names:'list[str]',dtype:Type)->None:
79
+ '''
80
+ 初始化
81
+ fname: 文件名
82
+ col_names: 列名
83
+ dtype: 数据类型, 可以是np.int32或np.float32
84
+ '''
85
+ super().__init__(open(fname,"w"),col_names,dtype)
86
+ self._fh.write(','.join(self._col_names)+"\n")
87
+
88
+ def write(self,data:list)->None:
89
+ self._fh.write(','.join(map(str,data))+"\n")
90
+
91
+ def write_all(self, data: np.ndarray)->None:
92
+ for ln in data: self.write(ln)
93
+
94
+ def close(self): self._fh.close()
95
+
96
+ class BinTableWriter(FileTableWriter):
97
+ '''二进制数据表写入器(抽象类)'''
98
+ _fh: IO
99
+ @abstractmethod
100
+ def __init__(self,fh,col_names:'list[str]',dtype:Type,buf_sz:int=1024):
101
+ super().__init__(fh,col_names,dtype)
102
+ header=_dtype2L(dtype)+('|'.join(self._col_names))
103
+ header+=" "*((4-len(header)%4)%4)
104
+ header=header.encode()
105
+ self._fh.write(struct.pack("<I",len(header)))
106
+ self._fh.write(header)
107
+ self._buf=[]
108
+ self._buf_sz=buf_sz
109
+ self._dcnt=0
110
+
111
+ def __wbuf(self):
112
+ self._fh.write(np.stack(self._buf,dtype=self._dtype).tobytes())
113
+ self._buf=[]
114
+
115
+ def write(self,data:list):
116
+ self._buf.append(data)
117
+ self._dcnt+=1
118
+ if len(data)>=self._buf_sz: self.__wbuf()
119
+
120
+ def write_all(self, data: np.ndarray):
121
+ if data.dtype!=self._dtype: data.astype(self._dtype)
122
+ self._fh.write(data.tobytes())
123
+
124
+ def close(self):
125
+ if len(self._buf)>0: self.__wbuf()
126
+ self._fh.close()
127
+
128
+ class SdtTableWriter(BinTableWriter):
129
+ '''SDT数据表写入器'''
130
+ def __init__(self,fname:str,col_names:'list[str]',dtype:Type,buf_sz:int=1024):
131
+ '''
132
+ 初始化
133
+ fname: 文件名
134
+ col_names: 列名
135
+ dtype: 数据类型, 可以是np.int32或np.float32
136
+ buf_sz: 缓冲区大小, 默认为1024
137
+ '''
138
+ super().__init__(open(fname,"wb"),col_names,dtype,buf_sz)
139
+
140
+ class SdtGzTableWriter(BinTableWriter):
141
+ '''SDT.GZ数据表写入器'''
142
+ def __init__(self,fname:str,col_names:'list[str]',dtype:Type,buf_sz:int=1024):
143
+ '''
144
+ 初始化
145
+ fname: 文件名
146
+ col_names: 列名
147
+ dtype: 数据类型, 可以是np.int32或np.float32
148
+ buf_sz: 缓冲区大小, 默认为1024
149
+ '''
150
+ super().__init__(gzip.open(fname,"wb"),col_names,dtype,buf_sz)
151
+
152
+ class TableReader(metaclass = ABCMeta):
153
+ '''数据表读取器(抽象类)'''
154
+ @abstractmethod
155
+ def __init__(self,col_names:'list[str]',dtype:Type)->None:
156
+ _dtypechk(dtype)
157
+ self._col_names:'list[str]'=col_names
158
+ self._col_cnt=len(self._col_names)
159
+ self._cmap = {cn:i for i,cn in enumerate(self._col_names)}
160
+ self._dtype:Type=dtype
161
+ @property
162
+ def head(self)->'list[str]':
163
+ '''表头'''
164
+ return self._col_names
165
+ @property
166
+ def dtype(self)->Type:
167
+ '''数据类型, 可以是np.int32或np.float32'''
168
+ return self._dtype
169
+ @abstractmethod
170
+ def read(self,cnt:int)->np.ndarray: '''从当前位置开始, 读取1行内容'''
171
+ @abstractmethod
172
+ def read_all(self)->np.ndarray: '''从头开始, 读取所有内容'''
173
+ @abstractmethod
174
+ def close(self)->None: '''关闭TableReader'''
175
+
176
+ class FileTableReader(TableReader):
177
+ '''文件数据表读取器(抽象类)'''
178
+ @abstractmethod
179
+ def __init__(self,f:IO,col_names:'list[str]',dtype:Type)->None:
180
+ super().__init__(col_names,dtype)
181
+ self._fh:IO=f
182
+ def close(self)->None:
183
+ self._fh.close()
184
+
185
+ class MemoryTableReader(TableReader):
186
+ '''内存数据表读取器'''
187
+ _data:np.ndarray
188
+ def __init__(self,col_names:'list[str]',data:np.ndarray):
189
+ '''
190
+ 初始化
191
+ col_names: 列名
192
+ data: 数据
193
+ '''
194
+ super().__init__(col_names,data.dtype)
195
+ self._data=data
196
+ self.__pos = 0
197
+
198
+ def read(self,cnt:int)->np.ndarray:
199
+ assert cnt > 0, "读取行数必须大于0"
200
+ ed = cnt + self.__pos
201
+ if ed > self._data.shape[0]: ed = self._data.shape[0]
202
+ return self._data[self.__pos:ed]
203
+
204
+ def read_all(self)->np.ndarray:
205
+ return self._data
206
+
207
+ def close(self): pass
208
+
209
+ class BinTableReader(FileTableReader):
210
+ '''二进制数据表读取器(抽象类)'''
211
+ _fh:BinaryIO
212
+ @abstractmethod
213
+ def __init__(self,fname:str,openFunc,allowNegativeSeek:bool=False)->None:
214
+ self.__fn=fname
215
+ self._openF=openFunc
216
+ fh:BinaryIO=self._openF(self.__fn,"rb")
217
+ hlen=struct.unpack("<I",fh.read(4))[0]
218
+ header=fh.read(hlen).decode()
219
+ super().__init__(fh,header[1:].strip().split('|'),_L2dtype(header[0]))
220
+ self._dstart=hlen+4
221
+ self._itmsz=self._col_cnt*4
222
+ self._neg=allowNegativeSeek
223
+
224
+ def seek(self,row:int,col:int=0)->int:
225
+ '''定位到第row行的第col个数据, row和col均从0开始编号'''
226
+ dst=self._dstart+self._itmsz*row+4*col
227
+ if self._neg or self._fh.tell()<=dst:
228
+ return self._fh.seek(dst)
229
+ else:
230
+ self._fh.close()
231
+ self._fh=self._openF(self.__fn,"rb")
232
+ return self._fh.seek(dst)
233
+
234
+ def read(self,cnt:int)->Optional[np.ndarray]:
235
+ data=self._fh.read(self._itmsz*cnt)
236
+ if data==b'': return None
237
+ return np.frombuffer(data,self._dtype).reshape(-1,self._col_cnt)
238
+
239
+ def read_all(self)->np.ndarray:
240
+ self.seek(0,0)
241
+ return np.frombuffer(self._fh.read(),self._dtype).reshape(-1,self._col_cnt)
242
+
243
+ def read_col(self,col_id:Union[int,str])->np.ndarray:
244
+ '''读取col_id列. col_id可以是下标, 也可以是列名'''
245
+ if isinstance(col_id,str): col_id=self._cmap[col_id]
246
+ i=0; dat=[]
247
+ while True:
248
+ self.seek(i,col_id)
249
+ d=self._fh.read(4)
250
+ if d==b'': break
251
+ dat.append(d)
252
+ i+=1
253
+ return np.frombuffer(b''.join(dat),dtype=self._dtype)
254
+
255
+ def read_at(self,start:int,cnt:int)->Optional[np.ndarray]:
256
+ '''读取range(start,start+cnt)范围的行'''
257
+ self.seek(start)
258
+ return self.read(cnt)
259
+
260
+ class SdtTableReader(BinTableReader):
261
+ '''SDT表格读取器'''
262
+ def __init__(self,fname:str):
263
+ '''
264
+ 初始化
265
+ fname: 文件名
266
+ '''
267
+ super().__init__(fname,open,True)
268
+
269
+ class SdtGzTableReader(BinTableReader):
270
+ '''SDT.GZ表格读取器'''
271
+ def __init__(self,fname:str):
272
+ '''
273
+ 初始化
274
+ fname: 文件名
275
+ '''
276
+ super().__init__(fname,gzip.open,False)
277
+
278
+ class CsvTableReader(FileTableReader):
279
+ '''CSV表格读取器'''
280
+ _fh:TextIO
281
+ def __init__(self,fname:str,dtype:Type)->None:
282
+ '''
283
+ 初始化
284
+ fname: 文件名
285
+ dtype: 数据类型, 可以是np.int32或np.float32
286
+ '''
287
+ self.__fn=fname
288
+ fh=open(fname,"r")
289
+ super().__init__(fh,fh.readline().strip().split(","),dtype)
290
+
291
+ def __parseline(self,ln:str)->np.ndarray:
292
+ strs=map(lambda x: x.strip(), ln.split(','))
293
+ if self._dtype==np.int32:
294
+ return np.array(list(map(int,strs)),dtype=np.int32)
295
+ else:
296
+ return np.array(list(map(float,strs)),dtype=np.float32)
297
+
298
+ def __read1(self)->Optional[np.ndarray]:
299
+ '''从当前位置开始, 读取1行'''
300
+ ln = self._fh.readline().strip()
301
+ if ln == "": return None
302
+ return self.__parseline(ln)
303
+
304
+ def read(self,cnt:int)->Optional[np.ndarray]:
305
+ if cnt == 1: return self.__read1()
306
+ ret = []
307
+ for _ in range(cnt):
308
+ dat = self.__read1()
309
+ if dat is None: break
310
+ ret.append(dat)
311
+ if len(ret) == 0: return None
312
+ return np.stack(ret)
313
+
314
+ def read_all(self)->np.ndarray:
315
+ self._fh.close()
316
+ self._fh = open(self.__fn,"r")
317
+ self._fh.readline()
318
+ return np.stack([self.__parseline(x) for x in self._fh.readlines()])
319
+
320
+ def createTableReader(fname:str,dtype:Optional[Type]=None)->TableReader:
321
+ '''从文件名创建TableReader, 对于CSV文件需要指定dtype'''
322
+ fn=fname.lower()
323
+ if fn.endswith(".csv"):
324
+ assert dtype is not None
325
+ return CsvTableReader(fname,dtype)
326
+ elif fn.endswith(".sdt"):
327
+ return SdtTableReader(fname)
328
+ elif fn.endswith(".sdt.gz"):
329
+ return SdtGzTableReader(fname)
330
+ else:
331
+ raise ValueError("不支持的文件类型")
332
+
333
+ def createTableWriter(fname:str,cols:'list[str]',dtype:Type)->TableWriter:
334
+ '''从文件名创建TableWriter'''
335
+ fn=fname.lower()
336
+ if fn.endswith(".csv"):
337
+ return CsvTableWriter(fname,cols,dtype)
338
+ elif fn.endswith(".sdt"):
339
+ return SdtTableWriter(fname,cols,dtype)
340
+ elif fn.endswith(".sdt.gz"):
341
+ return SdtGzTableWriter(fname,cols,dtype)
342
+ elif fn.endswith("<mem>"):
343
+ return MemoryTableWriter(cols,dtype)
344
+ else:
345
+ raise ValueError("不支持的文件类型")
346
+
347
+ def _convbinfile(r:Union[BinaryIO,gzip.GzipFile],w:Union[BinaryIO,gzip.GzipFile],bufsz:int=1024*1024*64):
348
+ while True:
349
+ data=r.read(bufsz)
350
+ if data==b'': break
351
+ w.write(data)
352
+ r.close()
353
+ w.close()
354
+
355
+ def convertTableFile(rfile:str,wfile:str,dtype:Optional[Type]=None,bufsz:int=1024)->None:
356
+ '''将一种文件类型的Table转化成另一文件类型的Table, 如果输入文件为CSV文件, 还需要指定dtype'''
357
+ if rfile.lower().endswith(".sdt.gz") and wfile.lower().endswith(".sdt"):
358
+ _convbinfile(gzip.open(rfile,"rb"),open(wfile,"wb"))
359
+ return
360
+ elif rfile.lower().endswith(".gz") and wfile.lower().endswith(".sdt.gz"):
361
+ _convbinfile(open(rfile,"rb"),gzip.open(wfile,"wb"))
362
+ return
363
+ r=createTableReader(rfile,dtype)
364
+ w=createTableWriter(wfile,r.head,r.dtype)
365
+ while True:
366
+ data=r.read(bufsz)
367
+ if data is None: break
368
+ w.write_all(data)
369
+ r.close()
370
+ w.close()
371
+
372
+ class ReadOnlyTable(Generic[_Table_DType]):
373
+ '''
374
+ **只读**数据表, 表中所有数据必须是同一类型, 必须是32位int或float格式.
375
+ 某一行的内容直接用下标获取; 某一列的内容用col方法获取.
376
+ 由于是只读列表,因此必须从文件加载. 支持的文件类型包含csv, sdt和sdt.gz.
377
+ '''
378
+ _d:'Optional[np.ndarray[tuple[int,int],np.dtype[_Table_DType]]]'
379
+ _btr:TableReader
380
+
381
+ @overload
382
+ def __init__(self,source:MemoryTableReader):
383
+ '''使用MemoryTableReader初始化'''
384
+ @overload
385
+ def __init__(self,source:MemoryTableWriter):
386
+ '''使用MemoryTableWriter初始化'''
387
+ @overload
388
+ def __init__(self,source:str,dtype:Optional[Table_DType]=None,preload:bool=False):
389
+ '''
390
+ 初始化
391
+ fname: 数据表文件名, 仅支持csv, sdt和sdt.gz文件.
392
+ dtype: 表格数据类型, 只有csv文件需要提供此项.
393
+ preload: 是否在初始化时预加载全部数据.
394
+
395
+ 注意:
396
+ 预加载使得初始化速度大幅降低, 但大幅提升了运行时性能.
397
+ csv强制开启预加载功能, sdt和sdt.gz自选(推荐sdt.gz开启, sdt关闭).
398
+ 如果数据过大导致内存不足, 请勿使用预加载功能.
399
+ '''
400
+
401
+ def __init__(self,source:Union[str,MemoryTableWriter,MemoryTableReader],dtype:Optional[Table_DType]=None,preload:bool=False):
402
+ if isinstance(source,str):
403
+ self._btr=createTableReader(source,dtype)
404
+ fn=source.lower()
405
+ if preload or fn.lower().endswith(".csv"):
406
+ self._d=self._btr.read_all()
407
+ else:
408
+ self._d=None
409
+ elif isinstance(source,MemoryTableReader):
410
+ self._btr=source
411
+ self._d=source._data
412
+ elif isinstance(source,MemoryTableWriter):
413
+ self._btr=MemoryTableReader(source._col_names,source._data)
414
+ self._d=source._data
415
+
416
+ @property
417
+ def head(self)->'list[str]':
418
+ '''表头'''
419
+ return self._btr._col_names
420
+
421
+ @property
422
+ def dtype(self)->Table_DType:
423
+ '''
424
+ 表中数据类型, 可取np.int32或np.float32
425
+ '''
426
+ return self._btr._dtype
427
+
428
+ @property
429
+ def data(self)->'np.ndarray[(int,int),np.dtype[_Table_DType]]':
430
+ '''
431
+ 表格数据. 如果没有预加载全部数据, 则调用此项时会加载所有数据.
432
+ '''
433
+ if self._d is None: self._d=self._btr.read_all()
434
+ return self._d
435
+
436
+ def force_load_all(self): self._d=self._btr.read_all()
437
+
438
+ @overload
439
+ def col(self,c:str): '''获取列名为c的列'''
440
+ @overload
441
+ def col(self,c:int): '''获取第c列'''
442
+ @overload
443
+ def col(self,c:Iterable[Union[str,int]]): '''获取多列'''
444
+
445
+ def col(self,c:Union[Iterable[Union[str,int]],str,int])->'np.ndarray[int,np.dtype[_Table_DType]]':
446
+ if isinstance(c,int) or isinstance(c,str):
447
+ if self._d is None:
448
+ assert isinstance(self._btr,BinTableReader)
449
+ return self._btr.read_col(c)
450
+ else:
451
+ if isinstance(c,str): c=self._btr._cmap[c]
452
+ return self._d[:,c]
453
+ elif isinstance(c,Iterable):
454
+ nc:list[int]=[self._btr._cmap[x] if isinstance(x,str) else x for x in c]
455
+ return self.data[:,nc]
456
+ else:
457
+ raise TypeError("不支持的索引类型")
458
+
459
+ def row(self,row_id:int)->'Optional[np.ndarray[int,np.dtype[_Table_DType]]]':
460
+ '''获取一行数据'''
461
+ if self._d is not None: return self._d[row_id]
462
+ assert isinstance(self._btr,BinTableReader)
463
+ return self._btr.read_at(row_id,1)
464
+
465
+ def at(self,col_name:str,row_id:int)->_Table_DType:
466
+ '''获取列名为col_name,行为row_id的数据'''
467
+ return self.data[self._btr._cmap[col_name],row_id]
468
+
469
+ def __getitem__(self,indices)->'Union[np.ndarray[tuple[int,...],np.dtype[_Table_DType]],_Table_DType]':
470
+ '''仅限数字下标'''
471
+ return self.data[indices]
472
+
473
+ def save(self,path:str):
474
+ '''
475
+ 保存到.csv或.bin.gz文件
476
+ path: 文件路径
477
+ '''
478
+ if self._d is None: self._d=self._btr.read_all()
479
+ fn=path.lower()
480
+ if fn.endswith(".csv"):
481
+ CsvTableWriter(path,self.head,self.dtype).write_all(self._d)
482
+ elif fn.endswith(".sdt"):
483
+ SdtTableWriter(path,self.head,self.dtype).write_all(self._d)
484
+ elif fn.endswith(".sdt.gz"):
485
+ SdtGzTableWriter(path,self.head,self.dtype).write_all(self._d)
486
+ else:
487
+ raise ValueError("不支持的文件名")
488
+
489
+ def __str__(self):
490
+ return ','.join(self.head)+"\n"+super().__str__()
feasytools/tfunc.py ADDED
@@ -0,0 +1,136 @@
1
+ from abc import ABCMeta, abstractmethod
2
+ from typing import Callable, Union
3
+ import bisect
4
+
5
+ class TimeFunc(metaclass=ABCMeta):
6
+ '''时变函数'''
7
+ @abstractmethod
8
+ def __call__(self,time:int)->float: ...
9
+ @abstractmethod
10
+ def __str__(self)->str: ...
11
+ def __add__(self,other:'FloatLike')->'TimeFunc':
12
+ return calcFunc(self,other,'+')
13
+ def __sub__(self,other:'TimeFunc')->'TimeFunc':
14
+ return calcFunc(self,other,'-')
15
+ def __mul__(self,other)->'TimeFunc':
16
+ return calcFunc(self,other,'*')
17
+ def __truediv__(self,other)->'TimeFunc':
18
+ return calcFunc(self,other,'/')
19
+
20
+ FloatLike = Union[TimeFunc,float]
21
+
22
+ class PlusFunc(TimeFunc):
23
+ '''和函数'''
24
+ def __init__(self,f1:TimeFunc,f2:TimeFunc): self._f1=f1; self._f2=f2
25
+ def __call__(self,_t:int)->float: return self._f1(_t)+self._f2(_t)
26
+ def __str__(self)->str: return f"<{self._f1}+{self._f2}>"
27
+
28
+ class MinusFunc(TimeFunc):
29
+ '''差函数'''
30
+ def __init__(self,f1:TimeFunc,f2:TimeFunc): self._f1=f1; self._f2=f2
31
+ def __call__(self,_t:int)->float: return self._f1(_t)-self._f2(_t)
32
+ def __str__(self)->str: return f"<{self._f1}-{self._f2}>"
33
+
34
+ class MulFunc(TimeFunc):
35
+ '''积函数'''
36
+ def __init__(self,f1:TimeFunc,f2:TimeFunc): self._f1=f1; self._f2=f2
37
+ def __call__(self,_t:int)->float: return self._f1(_t)*self._f2(_t)
38
+ def __str__(self)->str: return f"<{self._f1}*{self._f2}>"
39
+
40
+ class DivFunc(TimeFunc):
41
+ '''商函数'''
42
+ def __init__(self,f1:TimeFunc,f2:TimeFunc): self._f1=f1; self._f2=f2
43
+ def __call__(self,_t:int)->float: return self._f1(_t)/self._f2(_t)
44
+ def __str__(self)->str: return f"<{self._f1}/{self._f2}>"
45
+
46
+ class ConstFunc(TimeFunc):
47
+ '''常数函数'''
48
+ def __init__(self,const:float): self._val:float=const
49
+ def __call__(self,time:int)->float: return self._val
50
+ def __str__(self)->str: return f"Const<{self._val}>"
51
+
52
+ class SegFunc(TimeFunc):
53
+ '''分段常数函数'''
54
+ def __init__(self,time_line:'list[int]',data:'list[float]'):
55
+ if len(time_line) != len(data): raise ValueError(f"时间线长度{len(time_line)}和数据长度{len(data)}不一致")
56
+ for i in range(1,len(time_line)):
57
+ if time_line[i]<=time_line[i-1]:
58
+ raise ValueError(f"时间必须严格递增: [{i}]={time_line[i]}<=[{i-1}]={time_line[i-1]}")
59
+ self._tl = time_line
60
+ self._d = data
61
+
62
+ def __call__(self,time:int)->float:
63
+ if time < self._tl[0]: raise ValueError(f"时间{time}必须在开始时间{self._tl[0]}之后")
64
+ return self._d[bisect.bisect_right(self._tl, time) - 1]
65
+ def __str__(self)->str: return f"SegF<{len(self._d)} segs>"
66
+
67
+ class TimeImplictFunc(TimeFunc):
68
+ '''隐式时变函数, 即调用的时机决定了时间, 而无需在__call__时指定。__call__的参数time无效'''
69
+ def __init__(self,func:'Callable[[],float]'):self._f=func
70
+ def __call__(self,time:int)->float:return self._f()
71
+ def __str__(self)->str:return f"TImpF<{self._f}>"
72
+
73
+ class ComFunc(TimeFunc):
74
+ '''将普通Python函数包装成可运算函数'''
75
+ def __init__(self,func:'Callable[[int],float]'):self._f=func
76
+ def __call__(self,time:int)->float:return self._f(time)
77
+ def __str__(self)->str:return f"TImpF<{self._f}>"
78
+
79
+ class ManualFunc(TimeFunc):
80
+ '''手动指定常数函数'''
81
+ def __init__(self,init_val:float):self._v=init_val
82
+ def setManual(self,val:float):self._v=val
83
+ def __call__(self,time:int)->float:return self._v
84
+ def __str__(self)->str:return f"ManF<{self._v}>"
85
+
86
+ def __calc_c0(f1:Union[ConstFunc,float],f2:float,op:str)->float:
87
+ if isinstance(f1,float):
88
+ if op=='+': return f1+f2
89
+ elif op=='-': return f1-f2
90
+ elif op=='*': return f1*f2
91
+ elif op=='/': return f1/f2
92
+ else: raise ValueError(op)
93
+ elif isinstance(f1,ConstFunc):
94
+ if op=='+': return f1._val+f2
95
+ elif op=='-': return f1._val-f2
96
+ elif op=='*': return f1._val*f2
97
+ elif op=='/': return f1._val/f2
98
+ else: raise ValueError(op)
99
+ else: raise TypeError(f1)
100
+
101
+ def __calc_c1(f1:SegFunc,f2:float,op:str)->SegFunc:
102
+ if op=='+': return SegFunc(f1._tl,[d+f2 for d in f1._d])
103
+ elif op=='-': return SegFunc(f1._tl,[d-f2 for d in f1._d])
104
+ elif op=='*': return SegFunc(f1._tl,[d*f2 for d in f1._d])
105
+ elif op=='/': return SegFunc(f1._tl,[d/f2 for d in f1._d])
106
+ else: raise ValueError(op)
107
+
108
+ def __calc_c2(f1:TimeImplictFunc,f2:float,op:str)->TimeImplictFunc:
109
+ if op=='+': return TimeImplictFunc(lambda: f1._f() + f2)
110
+ elif op=='-': return TimeImplictFunc(lambda: f1._f() - f2)
111
+ elif op=='*': return TimeImplictFunc(lambda: f1._f() * f2)
112
+ elif op=='/': return TimeImplictFunc(lambda: f1._f() / f2)
113
+ else: raise ValueError(op)
114
+
115
+ def calcFunc(f1:FloatLike,f2:FloatLike,op:str)->TimeFunc:
116
+ if isinstance(f2,ConstFunc): f2=f2._val
117
+ if isinstance(f2,float):
118
+ if isinstance(f1,(ConstFunc,float)): return ConstFunc(__calc_c0(f1,f2,op))
119
+ elif isinstance(f1,SegFunc): return __calc_c1(f1,f2,op)
120
+ elif isinstance(f1,TimeImplictFunc): return __calc_c2(f1,f2,op)
121
+ if isinstance(f1,float): f1=ConstFunc(f1)
122
+ if isinstance(f1,ConstFunc) and op in ['+','*']:
123
+ if isinstance(f2,SegFunc): return __calc_c1(f2,f1._val,op)
124
+ elif isinstance(f2,TimeImplictFunc): return __calc_c2(f2,f1._val,op)
125
+ assert isinstance(f1,TimeFunc) and isinstance(f2,TimeFunc)
126
+ if op=='+': return PlusFunc(f1,f2)
127
+ elif op=='-': return MinusFunc(f1,f2)
128
+ elif op=='*': return MulFunc(f1,f2)
129
+ elif op=='/': return DivFunc(f1,f2)
130
+ else: raise ValueError(op)
131
+
132
+ def makeFunc(time_line:'list[int]',data:list)->TimeFunc:
133
+ '''生成分段常数函数或常数函数'''
134
+ if len(time_line)!=len(data): raise ValueError(f"时间线长度{len(time_line)}和数据长度{len(data)}不一致")
135
+ if len(data)==1: return ConstFunc(data[0])
136
+ else: return SegFunc(time_line,data)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 fmy_xfk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.1
2
+ Name: feasytools
3
+ Version: 0.0.1
4
+ Summary: A set of tools for data processing, including Time Function, Table, Priority Queue, Range List, etc.
5
+ Home-page: UNKNOWN
6
+ Author: fmy_xfk
7
+ License: UNKNOWN
8
+ Platform: UNKNOWN
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.6
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: numpy (>=1.19.0)
15
+
16
+ # FEasyTools: Some useful components
17
+ - dprov: Data provider as time functions
18
+ - argchk: Argument parser with type checking
19
+ - table: Table helper for the CSV, SDT & SDT.GZ format, and their reader and writer.
20
+ - pq: Heap, priority queue & buffered priority queue
21
+ - rangelist: An xml reader for a time range list
22
+
23
+
@@ -0,0 +1,11 @@
1
+ feasytools/__init__.py,sha256=m2X-Bi0ZoNW3aKB92oUXPY2siWYR3GUHRZUkyQMwTdY,255
2
+ feasytools/argchk.py,sha256=RzHkZ7e8RR316pGK7-V959mY6_RN9p78sxzitwe75ks,2296
3
+ feasytools/pq.py,sha256=AywPHXXKuuEE8g8UyhCn3bauaue6i15JfmN_EpqBJx8,5348
4
+ feasytools/rangelist.py,sha256=xhAG3a6suBfjZAeMfsV0ZdtWRmj4QGiRk6bWEHbQwf4,1897
5
+ feasytools/table.py,sha256=yOkszGB2W288CfUSh4dTxVFDkJKkXtE_Bt1lyHytP2Y,17755
6
+ feasytools/tfunc.py,sha256=ShWifoyf1GZW2734MwkyBDPUiTFdScd_u50EufiO2dw,6039
7
+ feasytools-0.0.1.dist-info/LICENSE,sha256=OLg9RcR578pNo70WxEqULMJckKU3tjBg7nfZ6lP5l_s,1085
8
+ feasytools-0.0.1.dist-info/METADATA,sha256=nUzbC7O5SJ7xRtaTAISsJ14UXiFIYSQtm4MjNQ0zdN8,788
9
+ feasytools-0.0.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
10
+ feasytools-0.0.1.dist-info/top_level.txt,sha256=jcLXDKDNavDkFqMJF_RhrIPofFeRwqtjGfvxtEY8W5o,11
11
+ feasytools-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.37.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ feasytools