-
Notifications
You must be signed in to change notification settings - Fork 72
/
Table.ahk
2950 lines (2752 loc) · 123 KB
/
Table.ahk
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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
####################################################################################################
####################################################################################################
###### ######
###### Function Library for 9/10 Table Manipulation ######
###### ######
####################################################################################################
####################################################################################################
AutoHotkey Version: 1.0.48.05
Language: English
Encoding: ANSI
Created On: 2010/11/15
Author: [VxE]
Lib Version: 0.27
Available @: http://www.autohotkey.com/forum/viewtopic.php?t=66290
VersionInfo: http://dl.dropbox.com/u/13873642/mufl_table.txt
These functions require a specific table format to function properly. A table must use a single
newline character to separate table rows from each other and from the table header. Each table
row must contain exactly the same number of tab characters as the table header. Any table that
does not conform to these restrictions is unlikely to work correctly with these functions.
Referencing Conventions:
When referencing columns or cells, functions in this library use the name of the column of
interest. To override this behavior and force the functions to reference columns or cells by
their ordinal position ( 1 = the first / leftmost ), precede the parameter's value with a
single tab character. Similarly, when referencing rows, functions in this library use the
contents of the first column of the row. To override this and use the row's ordinal position
instead ( 1 = the row just after the header ), precede the parameter's value with a single tab.
All comparisons are string-based* so be careful about using numeric row identifiers.
(*) NOTE: the following functions will perform non-string comparisons if comparing non-string
data: Table_GetRowIndex, Table_Between
Function List:
Table_Aggregate - Returns table metadata, typicaly derived from a single table column.
Table_Append - Appends rows, columns, or both to a table. Also manipulates table columns.
Table_Between - A simple QUERY. Removes table rows that don't satisfy an inequality.
Table_ColToList - Extracts a single column from the table in the form of a delimited list.
Table_Decode - Replaces character entities for tabs and newlines with the actual characters.
Table_Deintersect - Returns the input table with rows that match the criteria removed.
Table_Encode - Replaces tabs and newlines with character entities (similar to html entities).
Table_FromCSV - Converts a CSV table to a 9/10 table (literal tabs and newlines are encoded).
Table_FromHTML - Converts an HTML table into a 9/10 table
Table_FromINI - Converts the text of an INI file into a 2-column table (Section/Key,Value).
Table_FromListview - Returns a table with the contents of the default gui's active listview.
Table_FromLvHwnd - Returns a table with the contents of the listview and its header.
Table_FromXMLNode - Converts an XML node array into a 9/10 table (not a lossless conversion).
Table_FormatTime - Converts the values in the indicated column using the FormatTime command.
Table_GetCell - Returns the value of one cell in the table.
Table_GetColName - Returns the name of the table column at the given position.
Table_GetRowIndex - Returns a row's ordinal position in the table.
Table_Header - Returns the table's header. (The counterpart, 'Table_Decapitate', was removed)
Table_Intersect - Returns the input table with rows that don't match the criteria removed.
Table_Invert - Inverts the table so that the first column becomes the new header.
Table_Join - Adds columns from one table to another, arranging rows to match.
Table_Len - Returns the number of rows in the table.
Table_RemCols - Removes table columns, either by name or index.
Table_RemRows - Removes table rows, either by id or by index.
Table_Reverse - Reverses the row order of the table.
Table_RotateL - Rotates a table 90 degrees to the left.
Table_SetCell - Replaces the contents of one cell in the table and returns the modified table.
Table_SpacePad - Converts a 9/10 table into a space-padded table.
Table_Sort - Sorts the table based on the contents of one column and some user options.
Table_SubTable - Removes some rows from the table.
Table_ToCSV - Converts a table to CSV format. Tab and newline character entities are decoded.
Table_ToListview - Modifies the current listview using the given table.
Table_ToLvHWND - Modifies a listview using the given table.
Table_Transpose - Flips a table along its main diagonal.
Table_Update - Updates certain cells in a table using data from another table.
Table_UpdateAppend - Updates a table, then appends rows that didn't have a match in the table.
Table_Width - Returns the number of columns in the table.
*/
; [0721]
Table_Aggregate( Table, Column="`t1", Op="sum", GroupBy="", Round="" ) { ; -------------------------
; Returns metadata derived from the table. 'Op' determines the aggregate data operation. Valid
; values for 'Op' include 'Sum', 'Min', 'Max', 'Med', 'Avg', 'Span', 'Count', and 'Dev'.
; If 'GroupBy' is not blank, the operation is resolved for each unique value in the column specified
; in 'GroupBy' and the results are returned in a 2-column table with the left column having the
; group column name and the right column having the name of the data column. E.g: if the table
; has 3 columns (A B C) then Table_Aggregate( Table, "B", "Sum", "C" ) will yield a table with the
; header "C B" where the values in 'C' are unique and the values in 'B' are the sums for each group.
If Op NOT IN sum,min,max,med,avg,span,count,dev
Return "" ; error: invalid operation
oel := ErrorLevel, Groups := "`n", tagz := "0", Round |= 0
If Round IS NOT INTEGER
Round := ""
Loop, Parse, Table, `n, `r ; extract the relevant column values and maybe the groups too
If ( A_Index = 1 ) ; Handle the header
{
VarSetCapacity( Table, 0 )
StringReplace, header, A_LoopField, % "`t", % "`t", UseErrorLevel
ColCount := ErrorLevel, header := "`t" header "`t"
If Asc( Column ) != 9
{
StringLeft, Column, header, InStr( header, "`t" Column "`t" )
StringReplace, Column, Column, % "`t", % "`t", UseErrorLevel
Column := ErrorLevel - 1
}
Else Column := Round( SubStr( Column, 2 ) ) - 1
If ( Column < 0 ) || ( ColCount < Column )
Return "", ErrorLevel := oel ; Error: invalid column specified
If ( GroupBy != "" ) && Asc( GroupBy ) != 9
{
StringLeft, GroupBy, header, InStr( header, "`t" GroupBy "`t" )
StringReplace, GroupBy, GroupBy, % "`t", % "`t", UseErrorLevel
GroupBy := ErrorLevel - 1 <= ColCount ? ErrorLevel - 1 : 0 - 1
}
Else GroupBy := Round( SubStr( GroupBy, 2 ) ) - 1
; don't error if 'groupBy' is invalid... treat it as blank
}
Else ; handle the body
{
Table := A_Index - 1 ; keep count how many values we have
If ( Column = 0 )
StringLeft, vxe, A_LoopField, InStr( A_LoopField . "`t", "`t" ) - 1
Else If ( Column = ColCount )
StringTrimLeft, vxe, A_LoopField, InStr( A_LoopField, "`t", 0, 0 )
Else
{
StringGetPos, pos, A_LoopField, % "`t", L%Column%
StringTrimLeft, vxe, A_LoopField, pos + 1
StringLeft, vxe, vxe, InStr( vxe, "`t" ) - 1
}
Table_Aggregate_Value_%Table% := vxe ; keep the value in a pseudo-array member
If ( GroupBy < 0 ) ; If the user doesn't want groups, use a single group internally
Table_Aggregate_Group_%Table% := ""
Else ; otherwise, the user wants groups, keep the group value
{
If ( GroupBy = 0 )
StringLeft, Acu, A_LoopField, InStr( A_LoopField . "`t", "`t" ) - 1
Else If ( GroupBy = ColCount )
StringTrimLeft, Acu, A_LoopField, InStr( A_LoopField, "`t", 0, 0 )
Else
{
StringGetPos, pos, A_LoopField, % "`t", L%GroupBy%
StringTrimLeft, Acu, A_LoopField, pos + 1
StringLeft, Acu, Acu, InStr( Acu, "`t" ) - 1
}
Table_Aggregate_Group_%Table% := Acu
}
}
; Instead of bothering with the user's selected operation now, just calculate all of
; them (except deviation and median) since some operations require others. E.g: 'span' is the
; max minus the min and 'dev' requires the avg to be known.
Loop %Table% ; for each datum in the table...
{
If !( pos := InStr( Groups, "`n" Table_Aggregate_Group_%A_Index% "`n" ) )
{ ; zomg! we haven't seen this group item before!. So set most of the aggs to the item val
tagz := "" tagz + 1
Groups .= Table_Aggregate_Group_%A_Index% . "`n", Table_Aggregate_Count_%tagz% := 1
Table_Aggregate_Med_%tagz% := Table_Aggregate_Sum_%tagz% := Table_Aggregate_Min_%tagz% := Table_Aggregate_Max_%tagz% := Table_Aggregate_Avg_%tagz% := Table_Aggregate_Value_%A_Index%
Continue
}
StringLeft, Acu, Groups, pos
StringReplace, Acu, Acu, `n, `n, UseErrorLevel
Acu := ErrorLevel
Table_Aggregate_Count_%Acu% += 1, Table_Aggregate_Sum_%Acu% += Table_Aggregate_Value_%A_Index%
Table_Aggregate_Avg_%Acu% := Table_Aggregate_Avg_%Acu% * ( 1 - 1 / Table_Aggregate_Count_%Acu% ) + Table_Aggregate_Value_%A_Index% * ( 1 / Count%Acu% )
If ( Table_Aggregate_Value_%A_Index% < Table_Aggregate_Min_%Acu% )
Table_Aggregate_Min_%Acu% := Table_Aggregate_Value_%A_Index%
If ( Table_Aggregate_Value_%A_Index% > Table_Aggregate_Max_%Acu% )
Table_Aggregate_Max_%Acu% := Table_Aggregate_Value_%A_Index%
If ( Op = "med" ) ; only do the median thing if that's the operation
Table_Aggregate_Med_%Acu% .= "`n" . Table_Aggregate_Value_%A_Index%
}
If ( GroupBy < 0 ) ; Don't bother with any groups, just return the value
{
If Op IN sum,min,max,avg,count
vxe := Table_Aggregate_%Op%_%tagz%
Else If ( Op = "span")
vxe := Table_Aggregate_Max_%tagz% - Table_Aggregate_min_%tagz%
Else If ( Op = "med" )
{
If ( Table_Aggregate_Count_%tagz% = 1 )
vxe := Table_Aggregate_Max_%tagz%
Else If ( Table_Aggregate_Count_%tagz% = 2 )
vxe := Table_Aggregate_Max_%tagz% / 2 + Table_Aggregate_Min_%tagz% / 2
Else
{ ; the median is calculated by sorting the list, then if the list size is odd,
; returning the middle item. But if the size is even, the average of the two middle
; values is the median.
Sort, Table_Aggregate_Med_%tagz%, N
vxe := Table_Aggregate_Count_%tagz% - 1 & 1, Acu := ( Table_Aggregate_Count_%tagz% - 1 >> 1 )
StringGetPos, pos, Table_Aggregate_Med_%tagz%, `n, L%Acu%
StringTrimLeft, Table_Aggregate_Med_%tagz%, Table_Aggregate_Med_%tagz%, pos + 1
StringGetPos, pos, Table_Aggregate_Med_%tagz%, `n
StringMid, Acu, Table_Aggregate_Med_%tagz%, pos + 2, InStr( Table_Aggregate_Med_%tagz%, "`n", 0, pos + 2 ) - pos - 2
StringLeft, Table_Aggregate_Med_%tagz%, Table_Aggregate_Med_%tagz%, pos
vxe := ( vxe ? Table_Aggregate_Med_%tagz% / 2 + Acu / 2 : Table_Aggregate_Med_%tagz% )
}
}
Else If ( Op = "dev" ) ; standard deviation requires another traversal of the data
{
Loop, %Table%
If ( A_Index = 1 )
vxe := ( Table_Aggregate_Avg_%tagz% - Table_Aggregate_Value_%A_Index% ) * ( Table_Aggregate_Avg_%tagz% - Table_Aggregate_Value_%A_Index% )
Else vxe := vxe * ( 1 - 1 / A_Index ) + ( ( Table_Aggregate_Avg_%tagz% - Table_Aggregate_Value_%A_Index% )
* ( Table_Aggregate_Avg_%tagz% - Table_Aggregate_Value_%A_Index% ) ) * ( 1 / A_Index )
vxe := SQRT( vxe )
}
Return ( round = "" ? vxe : Round( vxe, Round ) ), ErrorLevel := oel
}
; Handle the 'GroupBy' part
Groups := SubStr( Groups, 2, -1 )
Loop, Parse, Groups, `n
{
If ( A_Index = 1 )
{
Column += 1, GroupBy += 1
StringGetPos, pos, header, % "`t", L%GroupBy%
StringTrimLeft, vxe, header, pos + 1
StringLeft, Groups, vxe, InStr( vxe, "`t" )
StringGetPos, pos, header, % "`t", L%Column%
StringTrimLeft, vxe, header, pos + 1
Groups .= SubStr( vxe, 1, InStr( vxe, "`t" ) - 1 )
}
Groups .= "`n" . A_LoopField . "`t"
If Op IN sum,min,max,avg,count
vxe := Table_Aggregate_%Op%_%A_Index%
Else If ( Op = "span" )
vxe := Table_Aggregate_Max_%A_Index% - Table_Aggregate_Min_%A_Index%
Else If ( Op = "med" )
{
If ( Table_Aggregate_Count_%A_Index% = 1 )
vxe := Table_Aggregate_Max_%A_Index%
Else If ( Table_Aggregate_Count_%A_Index% = 2 )
vxe := Table_Aggregate_Max_%A_Index% / 2 + Table_Aggregate_Min_%A_Index% / 2
Else
{
Sort, Table_Aggregate_Med_%A_Index%, N
vxe := Table_Aggregate_Count_%A_Index% - 1 & 1, Acu := ( Table_Aggregate_Count_%A_Index% - 1 >> 1 )
StringGetPos, pos, Table_Aggregate_Med_%A_Index%, `n, L%Acu%
StringTrimLeft, Table_Aggregate_Med_%A_Index%, Table_Aggregate_Med_%A_Index%, pos + 1
StringGetPos, pos, Table_Aggregate_Med_%A_Index%, `n
StringMid, Acu, Table_Aggregate_Med_%A_Index%, pos + 2
, InStr( Table_Aggregate_Med_%A_Index%, "`n", 0, pos + 2 ) - pos - 2
StringLeft, Table_Aggregate_Med_%A_Index%, Table_Aggregate_Med_%A_Index%, pos
vxe := vxe ? Table_Aggregate_Med_%A_Index% / 2 + Acu / 2 : Table_Aggregate_Med_%A_Index%
}
}
Else If ( Op = "dev" ) ; standard deviation requires another traversal of the data
{
Acu := Table_Aggregate_Avg_%A_Index%
Loop, Parse, Table_Aggregate_Med_%A_Index%, `n
If ( A_Index = 1 )
vxe := ( Acu - A_LoopField ) * ( Acu - A_LoopField )
Else vxe := vxe * ( 1 - 1 / A_Index ) + ( ( Acu - A_LoopField )
* ( Acu - A_LoopField ) ) * ( 1 / A_Index )
vxe := SQRT( vxe )
}
Groups .= Round = "" ? vxe : Round( vxe, Round )
}
Return Groups, ErrorLevel := oel
} ; Table_Aggregate( Table, Column="`t1", Op="sum", GroupBy="", Round="" ) -------------------------
Table_Append( TableA, TableB, Mode=0 ) { ; ---------------------------------------------------------
; Appends TableB to TableA, optionally adding columns to TableA. Mode determines whether columns
; are added and how rows are added. The following table details the supported values of Mode:
; Mode = 0 -> Rows from TableB are added to TableA, no columns are changed in TableA
; Mode = 1 -> Rows from TableB are added to TableA, columns in TableB are added to TableA's right
; side IF TableA doesn't already have that column.
; Mode = 2 -> Columns in TableB are added to TableA's right side IF TableA doesn't already have
; that column, no rows are added to TableA.
; NOTE: Rows appended are re-arranged so that the column data matches up with TableA's columns.
Table_Append_Cell_0x1 := Table_Append_Cell_1 := ""
oel := ErrorLevel, Mode |= 0
TableA .= "`n"
StringGetPos, pos, TableA, `n
StringLeft, HeaderA, TableA, pos
StringTrimRight, HeaderA, HeaderA, SubStr( HeaderA, 0 ) = "`r"
StringTrimRight, TableA, TableA, 1
TableB .= "`n"
StringGetPos, pos, TableB, `n
StringLeft, HeaderB, TableB, pos
StringTrimRight, HeaderB, HeaderB, SubStr( HeaderB, 0 ) = "`r"
StringTrimLeft, TableB, TableB, pos
StringTrimRight, TableB, TableB, 1
If ( HeaderA != HeaderB ) ; Headers are NOT identical... so check the columns
{
HeaderA := "`t" HeaderA "`t"
; See if we need to add any columns to TableA
If ( Mode = 1 || Mode = 2 )
Loop, Parse, HeaderB, % "`t"
IfNotInString, HeaderA, % "`t" A_LoopField "`t"
{
HeaderA .= "`n" A_LoopField "`t"
TableA .= "`n"
StringReplace, TableA, TableA, `n, % "`t`n", A
StringReplace, TableA, TableA, % "`t`n", % "`t" A_LoopField "`n"
StringTrimRight, TableA, TableA, 1
}
If ( Mode = 0 || Mode = 1 ) ; Add rows to TableA
{
; Generate a column arrangement map based on the headers
StringReplace, HeaderA, HeaderA, `n,, A
StringTrimRight, HeaderA, HeaderA, 1
StringTrimLeft, HeaderA, HeaderA, 1
StringTrimLeft, TableB, TableB, 1
HeaderB := "`t" HeaderB "`t"
Table_Append_Cell_0x1 := 0
Loop, Parse, HeaderA, % "`t", `r
{
StringGetPos, pos, HeaderB, % "`t" A_LoopField "`t"
If ( ErrorLevel := !ErrorLevel )
{
Table_Append_Cell_0x1 := 1
StringReplace, HeaderB, HeaderB, % "`t" A_LoopField "`t", % "`t`t"
StringLeft, Table_Append_Cell_1, HeaderB, pos + 1
StringReplace, Table_Append_Cell_1, Table_Append_Cell_1, % "`t", % "`t", UseErrorLevel
}
If ( A_Index = 1 )
HeaderA := ErrorLevel
Else HeaderA .= "`n" ErrorLevel
}
If ( Table_Append_Cell_0x1 != 0 )
{
Table_Append_Cell_1 := ""
Loop, Parse, TableB, `n, `r
{
Loop, Parse, A_LoopField, % "`t"
Table_Append_Cell_%A_Index% := A_LoopField
Loop, Parse, HeaderA, `n
{
TableA .= ( A_Index = 1 ? "`n" : "`t" )
If ( A_LoopField )
{
TableA .= Table_Append_Cell_%A_LoopField%
Table_Append_Cell_%A_LoopField% := ""
}
}
}
}
}
}
Else If ( Mode = 0 || Mode = 1 ) ; identical headers, no need to futz around with columns.
TableA .= TableB
Return TableA, ErrorLevel := oel
} ; Table_Append( TableA, TableB, Mode=0 ) ---------------------------------------------------------
Table_Between( Table, Column, GreaterThan, LessThan="" ) { ; ---------------------------------------
; Returns Table, with any rows in which the 'Column' cell exceeds the indicated bounds removed. In
; other words, it returns the table rows that have a value between 'GreaterThan', and 'LessThan'.
; To invert the operation (return rows that are NOT between the two values), use a value for
; 'GreaterThan' that is greater than the value of 'LessThan'.
; To invert the equality portion of a check, preceed the intended value with a single tab character.
; To perform a simple inequality check, leave the other value blank (e.g: set 'GreaterThan' to ""
; for the function to remove rows with a value greater than the value in 'LessThan' ).
; Example: get rows where 4 <= Cost <= 5 : Table_Between( Items, "Cost", 4, 5 )
; Example: get rows where 0 <= Cost : Table_Between( Items, "Cost", "0" )
; Example: get rows where Cost <= 0 OR Cost > 100 : Table_Between( Items, "Cost", 100, "`t0" )
; Example: get rows where Cost <= 0 : Table_Between( Items, "Cost", "", "0" )
StringTrimLeft, GreaterThan, GreaterThan, gt := 1 = InStr( GreaterThan, "`t" )
StringTrimLeft, LessThan, LessThan, lt := 1 = InStr( LessThan, "`t" )
StringLen, gz, GreaterThan
StringLen, lz, LessThan
oel := ErrorLevel, nix := gz && lz && LessThan < GreaterThan
Loop, Parse, Table, `n, `r ; Parse the table by newlines, removing carriage returns.
If ( A_Index = 1 ) ; first line is the header, so find the matchcol
{
StringReplace, Table, A_LoopField, % "`t", % "`t", UseErrorLevel
ColCount := ErrorLevel
If Asc( Column ) != 9 ; column by name
{
pos := "`t" A_LoopField
StringLeft, Column, pos, InStr( pos "`t", "`t" Column "`t" )
StringReplace, Column, Column, % "`t", % "`t", UseErrorLevel
Column := ErrorLevel - 1
}
Else Column := Round( SubStr( Column, 2 ) ) - 1 ; column by index
If ( Column < 0 || ColCount < Column )
Return A_LoopField, ErrorLevel := oel ; column not found, so return empty table
}
Else ; lines other than the first contain row data
{
If !Column ; the first cell
StringLeft, cell, A_LoopField, InStr( A_LoopField "`t", "`t" ) - 1
Else If ( Column = ColCount ) ; the last cell
StringTrimLeft, cell, A_LoopField, InStr( A_LoopField, "`t", 0, 0 )
Else ; cell is somewhere in the middle
{
StringGetPos, pos, A_LoopField, % "`t", L%Column%
StringTrimLeft, cell, A_LoopField, pos + 1
StringLeft, cell, cell, InStr( cell, "`t" ) - 1
}
; nested ternary operators FTW ! I'm pretty sure the ternaries here are faster than
; the boolean-only equivalent due to fewer variable dereferences.
If ( nix ? ( ( lz && ( lt ? ( cell <= LessThan ) : ( cell < LessThan ) ) )
|| ( gz && ( gt ? ( GreaterThan <= cell ) : ( GreaterThan < cell ) ) ) )
: ( ( !gz || ( gt ? ( GreaterThan < cell ) : ( GreaterThan <= cell ) ) )
&& ( !lz || ( lt ? ( cell < LessThan ) : ( cell <= LessThan ) ) ) ) )
Table .= "`n" A_LoopField
}
Return Table, ErrorLevel := oel ; return the OK rows
} ; Table_Between( Table, Column, GreaterThan, LessThan="" ) ---------------------------------------
Table_ColToList( Table, Column="`t1", Delimiters="`n" ) { ; ----------------------------------------
; Extracts a single column from the table and formats it with the specified delimiters.
; If 'Delimiters' begins with the word 'multi', then the character immediately following the 'i' is
; used to parse the delimiters list, and they are used to delimit the cells in order. For example,
; if 'Delimiters' contained "multi,.,.,|", and the table had 9 rows then the resulting list would
; look like this: "<data1>.<data2>.<data3>|<data4>.<data5>.<data6>|<data7>.<data8>.<data9>"
oel := ErrorLevel
Loop, Parse, Table, `n, `r
If ( A_Index = 1 ) ; look in the header for the indicated column
{
StringReplace, Table, A_LoopField, % "`t", % "`t", UseErrorLevel
ColCount := ErrorLevel, Table := ""
If Asc( Column ) != 9 ; column by name
{
pos := "`t" A_LoopField
StringLeft, Column, pos, InStr( pos "`t", "`t" Column "`t" )
StringReplace, Column, Column, % "`t", % "`t", UseErrorLevel
Column := ErrorLevel - 1
}
Else Column := Round( SubStr( Column, 2 ) ) - 1 ; column by index
If ( Column < 0 || ColCount < Column )
Return "", ErrorLevel := oel ; column not found, so return nothing
If InStr( Delimiters, "multi" ) = 1 ; check for multi-delimiter
Loop, Parse, Delimiters, % SubStr( Delimiters, 6, 1 )
DelCount := A_Index - 1, d%DelCount% := A_LoopField
Else DelCount := A_Index, d%DelCount% := Delimiters
Delimiters := 0 ; initialize the delimiter cycle
}
Else
{
If !( Column )
StringLeft, pos, A_LoopField, InStr( A_LoopField . "`t", "`t" ) - 1
Else If ( Column = ColCount )
StringTrimLeft, pos, A_LoopField, InStr( A_LoopField, "`t", 0, 0 )
Else
{
StringGetPos, pos, A_LoopField, % "`t", L%Column%
StringTrimLeft, pos, A_LoopField, pos + 1
StringLeft, pos, pos, InStr( pos, "`t" ) - 1
}
If !( Delimiters ) ; this only happens for the first row
Table := pos
Else Table .= d%Delimiters% . pos
Delimiters := 1 + Mod( Delimiters, DelCount ) ; for single-delimiter, it's always '1'
}
Return Table, ErrorLevel := oel
} ; Table_ColToList( Table, Column="`t1", Delimiters="`n" ) ----------------------------------------
Table_Decode( String ) { ; -------------------------------------------------------------------------
; Returns the string with the following entities decoded into their ascii characters: 
`; 	`;
; NOTE: It is the user's responsibility to manage character escaping when using table-functions
oel := ErrorLevel
StringReplace, String, String, 
`;, `n, a
StringReplace, String, String, 	`;, % "`t", a
StringReplace, String, String, 	`;, % "`t", a
Return String, ErrorLevel := oel
} ; Table_Decode( String ) -------------------------------------------------------------------------
Table_Deintersect( TableA, TableB, MatchColA="", MatchColB="" ) { ; --------------------------------
; Returns TableA, with any row with a value that matches a value in TableB's match column removed.
; If MatchColB is blank, it is treated as the same column NAME as MatchColA. If MatchColA is blank,
; it is considered to be the NAME of TableB's first column (this makes it easier to deintersect a
; table using a simple list of values). If the value of 'TableB' is a positive integer, the returned
; table will contain only the rows with a MatchColA value that has fewer than that many instances
; in the same column above it. For example, Table_Deintersect( MyTable, "1", "`t1" ) will remove any
; row whose first member is a repeat of a previous row's first member. In other words, it removes
; duplicates. Similarly Table_Deintersect( MyTable, "2", "`t1" ) will allow up to 2 rows to have the
; same first member, but any subsequent rows with the same value in their first member are removed.
; When this mode is used, the default value for 'MatchColA' is TableA's first column.
oel := ErrorLevel
If TableB IS INTEGER
MatchColB := "`n"
Else
{
TableB .= "`n"
StringGetPos, pos, TableB, `n
StringLeft, HeaderB, TableB, pos
StringTrimRight, HeaderB, HeaderB, pos = InStr( HeaderB, "`r", 0, 0 )
}
StringLen, len, TableA
Loop, Parse, TableA, `n, `r
If ( A_Index = 1 ) ; figure out what kind of deintersection we're doing here
{
StringReplace, ColCount, A_LoopField, % "`t", % "`t", UseErrorLevel
ColCount := ErrorLevel
If ( MatchColA = "" )
If ( MatchColB != "`n" ) ; MatchColA defaults to TableB's 1st col
StringLeft, MatchColA, HeaderB, InStr( HeaderB "`t", "`t" ) - 1
Else StringLeft, MatchColA, A_LoopField, InStr( A_LoopField "`t", "`t" ) - 1
If Asc( MatchColA ) != 9 ; column by name
{
vxe := "`t" A_LoopField
StringLeft, MatchColA, vxe, InStr( vxe "`t", "`t" MatchColA "`t" )
StringReplace, MatchColA, MatchColA, % "`t", % "`t", UseErrorLevel
MatchColA := ErrorLevel - 1
}
Else MatchColA := Round( SubStr( MatchColA, 2 ) ) - 1 ; column by index
If ( MatchColA < 0 || ColCount < MatchColA )
Return TableA, ErrorLevel := oel ; column not found, so return the table
If ( MatchColB = "`n" )
MatchColB := 0 - TableB, VarSetCapacity( TableB, len / ColCount << !!A_IsUnicode, 0 )
Else
{
If ( MatchColB = "" ) ; if MatchColB is blank, use MatchColA's name as MatchColB
{
If !( MatchColA )
StringLeft, MatchColB, A_LoopField, InStr( A_LoopField "`t", "`t" ) - 1
Else If ( MatchColA = ColCount )
StringTrimLeft, MatchColB, A_LoopField, InStr( A_LoopField, "`t", 0, 0 )
Else
{
StringGetPos, pos, A_LoopField, % "`t", L%MatchColA%
StringTrimLeft, MatchColB, A_LoopField, pos + 1
StringLeft, MatchColB, MatchColB, InStr( MatchColB, "`t" ) - 1
}
}
If Asc( MatchColB ) != 9 ; column by name
{
vxe := "`t" HeaderB
StringLeft, MatchColB, vxe, InStr( vxe "`t", "`t" MatchColB "`t" )
StringReplace, MatchColB, MatchColB, % "`t", % "`t", UseErrorLevel
MatchColB := ErrorLevel - 1
}
Else MatchColB := Round( SubStr( MatchColB, 2 ) ) - 1 ; column by index
StringReplace, HeaderB, HeaderB, % "`t", % "`t", UseErrorLevel
If ( MatchColB < 0 || ErrorLevel < MatchColB )
Return TableA, ErrorLevel := oel ; column not found, so return the table
Loop, Parse, TableB, `n, `r ; reduce TableB to just its match col (in list form)
If ( A_Index = 1 )
{
StringReplace, TableB, A_LoopField, % "`t", % "`t", UseErrorLevel
ColCountB := ErrorLevel
TableB := "`n"
}
Else
{
If !( MatchColB )
StringLeft, vxe, A_LoopField, InStr( A_LoopField "`t", "`t" ) - 1
Else If ( MatchColB = ColCountB )
StringTrimLeft, vxe, A_LoopField, InStr( A_LoopField, "`t", 0, 0 )
Else
{
StringGetPos, pos, A_LoopField, % "`t", L%MatchColB%
StringTrimLeft, vxe, A_LoopField, pos + 1
StringLeft, vxe, vxe, InStr( vxe, "`t" ) - 1
}
TableB .= vxe "`n"
}
StringTrimRight, TableB, TableB, 1
}
; for a multiples deintersection, the matchlist is generated as the table is parsed.
TableA := A_LoopField ; column found OK
}
Else If ( A_LoopField = "" )
Continue
Else If ( MatchColB < 0 ) ; multiples deintersection
{
If !( MatchColA )
StringLeft, vxe, A_LoopField, InStr( A_LoopField "`t", "`t" ) - 1
Else If ( MatchColA = ColCount )
StringTrimLeft, vxe, A_LoopField, InStr( A_LoopField, "`t", 0, 0 )
Else
{
StringGetPos, pos, A_LoopField, % "`t", L%MatchColA%
StringTrimLeft, vxe, A_LoopField, pos + 1
StringLeft, vxe, vxe, InStr( vxe, "`t" ) - 1
}
StringReplace, TableB, TableB, `n%vxe%`n, `n%vxe%`n, UseErrorLevel
If ( ErrorLevel + MatchColB < 0 ) ; previous count minus the allowed count
{
TableA .= "`n" A_LoopField
TableB .= "`n" vxe "`n"
}
}
Else ; normal deintersection
{
If !( MatchColA )
StringLeft, pos, A_LoopField, InStr( A_LoopField "`t", "`t" ) - 1
Else If ( MatchColA = ColCount )
StringTrimLeft, pos, A_LoopField, InStr( A_LoopField, "`t", 0, 0 )
Else
{
StringGetPos, pos, A_LoopField, % "`t", L%MatchColA%
StringTrimLeft, pos, A_LoopField, pos + 1
StringLeft, pos, pos, InStr( pos, "`t" ) - 1
}
IfNotInString, TableB, `n%pos%`n
TableA .= "`n" A_LoopField
}
Return TableA, ErrorLevel := oel
} ; Table_Deintersect( TableA, TableB, MatchColA="", MatchColB="" ) --------------------------------
Table_Encode( String ) { ; -------------------------------------------------------------------------
; Return the string with all newlines and tabs converted into html-entities 0; &#09;
; This is the only way to validly insert text which may contain these characters into a 9/10 table.
; NOTE: It is the user's responsibility to manage character escaping when using table-functions
oel := ErrorLevel
StringReplace, String, String, `n, 
`;, A
StringReplace, String, String, % "`t", 	`;, A
Return String, ErrorLevel := oel
} ; Table_Encode( String ) -------------------------------------------------------------------------
Table_FormatTime( Table, Format="", Columns="`t" ) { ; ---------------------------------------------
; Returns a table where any instance of a 14-digit date string in one of the indicated columns is
; converted to 'Format' using FormatTime. If 'Columns' is a single tab character, this function will
; autodetect 14-digit cell values in the table and convert any that it finds.
; NOTE: multiple columns can be converted by separating the column identifiers with newlines.
; NOTE: This function is intended to prep a table for insertion into a listview with Table_ToListview
oel := ErrorLevel
Loop, Parse, Table, `n, `r ; parse the table
If ( A_Index = 1 ) ; handle the header
{
StringReplace, vxe, A_LoopField, % "`t", % "`t", UseErrorLevel
ColCount := ErrorLevel, vxe := "`t" vxe "`t"
If !( auto := Columns = "`t" )
Loop, Parse, Columns, `n, `r
{
If ( A_Index = 1 )
Columns := "`t"
If Asc( A_LoopField ) != 9
{
StringLeft, pos, vxe, InStr( vxe, "`t" A_LoopField "`t" )
StringReplace, pos, pos, % "`t", % "`t", UseErrorLevel
Columns .= ErrorLevel "`t"
}
Else If ( 0 < ( pos := Round( SubStr( A_LoopField, 2 ) ) ) )
&& ( pos <= ColCount )
Columns .= pos "`t"
}
If !auto && Columns = "`t"
Return Table, ErrorLevel := oel ; none of the specified columns were found...
Table := A_LoopField
}
Else Loop, Parse, A_LoopField, % "`t"
{
vxe := A_LoopField
If ( auto )
{
If vxe IS TIME
FormatTime, vxe, %vxe%, %Format%
}
Else If InStr( Columns, "`t" A_Index "`t" ) && ( vxe != "" )
FormatTime, vxe, %vxe%, %Format%
Table .= ( A_Index = 1 ? "`n" : "`t" ) vxe
}
Return Table, ErrorLevel := oel
} ; Table_FormatTime( Table, Format="", Columns="`t" ) ---------------------------------------------
Table_FromCSV( CSV_Table ) { ; ---------------------------------------------------------------------
; Converts a CSV table into a 9/10 table. Literal tabs and newlines are encoded as 	`; and 
`;
oel := ErrorLevel
StringReplace, CSV_Table, CSV_Table, % "`t", 	`;, A
Loop, Parse, CSV_Table, " ; look for literal newlines that need to be escaped
If !( A_Index & 1 ) ; inside quotes
{
StringReplace, vxe, A_LoopField, `n, 
`;, A
CSV_Table .= """" vxe
}
Else If ( A_Index = 1 )
CSV_Table := A_LoopField
Else CSV_Table .= """" A_LoopField
Loop, Parse, CSV_Table, `n, `r ; parse by non-literal newlines
If ( A_Index = 1 )
Loop, Parse, A_LoopField, CSV
CSV_Table := ( A_Index = 1 ? "" : CSV_Table "`t" ) A_LoopField
Else If ( A_LoopField != "" )
Loop, Parse, A_LoopField, CSV
CSV_Table .= ( A_Index = 1 ? "`n" : "`t" ) A_LoopField
Return CSV_Table, ErrorLevel := oel
} ; Table_FromCSV( CSV_Table ) ---------------------------------------------------------------------
Table_FromHTML( HTML, StartChar=1 ) { ; ------------------------------------------------------------
; Converts an HTML table (inside <Table></Table> tags) into a 9/10 table. Nested tables remain
; unconverted. Literal linebreaks (that means linebreaks in the HTML code) are removed and any <br>
; tags are left unchanged.
; NOTE: 'StartChar' can be used to determine which table to convert if the HTML contains more than one
; NOTE: Literal tabs are converted into '	`;' in the returned table.
; NOTE: Cell spanning is handled by replicating the cell's contents into each of the spanned cells.
; NOTE: The table's first row becomes the header of the retuned table. To supply a different header,
; simply append this function's return value to it, separated by a newline character.
; NOTE: <thead> and <tfoot> sections are NOT respected by this function.
oel := ErrorLevel
StringGetPos, pos, HTML, <table,, StartChar - 1
If ( ErrorLevel )
Return ""
StringTrimLeft, HTML, HTML, pos
StringReplace, HTML, HTML, `r,, A
nest := row := col := i := j := w := 0
spans := "`t"
Loop, Parse, HTML, <
If ( A_Index != 1 )
{
Loop, Parse, A_LoopField, >, % " `t`r`n"
If ( A_Index = 1 )
StringReplace, tag, A_LoopField, `n,, A
Else StringReplace, tex, A_LoopField, `n,, A
If !( nest += ( InStr( tag " ", "table " ) = 1 ) - ( InStr( tag " ", "/table " ) = 1 ) )
Break ; we've found the close-tag for the top table
Else If ( nest != 1 ) ; we're still inside a nested table
HTML .= "<" tag ">" tex
Else If InStr( tag " ", "tr " ) = 1 ; it's a new row
row += ( col := 1 )
Else If InStr( tag " ", "/tr " ) = 1
w := w < ( Table_FromHTML_Row_%row% := col ) ? col : w
Else If InStr( tag " ", "td " ) = 1 || InStr( tag " ", "th " ) = 1 ; it's a new cell
{
RegexMatch( tag " colspan=1", "\hcolspan=""?\K\d+", colspan )
RegexMatch( tag " rowspan=1", "\hrowspan=""?\K\d+", rowspan )
If !( colspan )
colspan := w - col
HTML := tex
}
Else If ( InStr( tag " ", "/td " ) = 1 || InStr( tag " ", "/th " ) = 1 ) && ( tag != lasttag ) ; end of cell
{
Loop, % rowspan
Loop, % colspan + !( B_Index := A_Index )
{
Loop
IfInString, spans, % "`t" ( row - 1 + B_Index ) " " ( col - 1 + A_Index ) "`t"
col += 1
Else Break
j := col - 1 + A_Index
If ( row < ( i := row - 1 + B_Index ) )
spans .= i " " j "`t"
StringReplace, Table_FromHTML_Cell_%i%_%j%, HTML, % "`t", % "	", A
}
col += colspan
}
Else HTML .= "<" tag ">" tex
lasttag := tag
}
; Now we have all of the html table cells in a pseudo array, so we'll assemble them into a table
Loop, %row%
Loop % w + !( row := A_Index )
If ( row = 1 ) && ( A_Index = 1 )
HTML := Table_FromHTML_Cell_%row%_%A_Index%
Else If ( A_Index > Table_FromHTML_Row_%row% )
HTML .= "`t"
Else HTML .= ( A_Index = 1 ? "`n" : "`t" ) Table_FromHTML_Cell_%row%_%A_Index%
; Lastly, decode the 5 main HTML entities
StringReplace, HTML, HTML, ", ", A
StringReplace, HTML, HTML, ', ', A
StringReplace, HTML, HTML, <, <, A
StringReplace, HTML, HTML, >, >, A
StringReplace, HTML, HTML, &, &, A
Return HTML, ErrorLevel := oel
} ; Table_FromHTML( HTML, StartChar=1 ) ------------------------------------------------------------
Table_FromINI( INI_File_Text, cd1="/" ) { ; --------------------------------------------------------
; Given text in ini format, this function creates a table such that the rightmost column holds the
; key values and the other column(s) hold the section name and keyname (separated by 'cd1'). By
; setting 'cd1' to a single tab (`t), the output table will have 3 columns (Section,Key,Value). By
; setting 'cd1' to a forward slash (default), the output table's first column will hold both the
; section name and keyname, with a slash between them (allowing simpler lookup operations).
oel := ErrorLevel, block_comment := 0, section := "Format Error: No Section Name"
Loop, Parse, INI_File_Text, `n
{
If ( A_Index = 1 )
INI_File_Text := "Section" cd1 "Key`tValue"
line := A_LoopField
StringLeft, line, line, InStr( line " `;", " `;" ) - 1
StringLeft, line, line, InStr( line "`t;", "`t;" ) - 1
If !( block_comment := ( block_comment | ( line = "/*" ) ) & ( line != "*/" ) )
Loop, Parse, line, `n, % "`r `t"
If SubStr( A_LoopField, 1, 1 ) = "[" && SubStr( A_LoopField, 0 ) = "]"
StringMid, section, A_LoopField, 2, StrLen( A_LoopField ) - 2
Else IfInString, A_LoopField, =
{
StringReplace, line, A_LoopField, =, `n ; replace the first '=' sign
Loop, Parse, line, `n, % "`r `t"
If ( A_Index = 1 )
INI_File_Text .= "`n" section cd1 A_LoopField "`t"
Else
{
StringReplace, line, A_LoopField, % "`t", 	`;, A
StringReplace, line, line, ```;, `;, A
INI_File_Text .= line
}
}
}
Return INI_File_Text, ErrorLevel := oel
} ; Table_FromINI( INI_File_Text, cd1="/" ) -------------------------------------------------------
Table_FromListview( scf="" ) { ; -------------------------------------------------------------------
; Returns a 9/10 table representing the default gui's current listview. To obtain a table from a
; different listview, use ControlGet with the 'List' option.
; If 'scf' is blank, the entire listview is considered. If 'scf' contains one or more of the letters
; 'sScCfF', the output is modified: the letters stand for 'select', 'check' and 'focus', and
; the capital letters add columns with the corresponding name to the right side of the table. So, if
; 'scf' contained 'sF', the returned table would contain only the listview rows that were selected,
; and a column named 'focused' would be added to the right side of the table and any row that was
; both selected AND focused would have the text 'focused' appear in that column.
; NOTE: multiple lowercase options are joined using 'AND' logic.
Static sv_s := "`tSelect", sv_c := "`tCheck", sv_f := "`tFocus", svl := "sScCfF" ; these are CONST
oel := ErrorLevel
Loop, Parse, svl ; look for options in the parameter
frow := Chr( 96 + A_Index ), _%frow% := !InStr( scf, A_LoopField, 1 )
ColCount := LV_GetCount( "Column" )
VarSetCapacity( Table, 16 * ColCount * LV_GetCount() << !!A_IsUnicode, 0 ) ; guesstimate size
Loop, % ColCount ; build the table header
If LV_GetText( scf, 0, A_Index )
Table .= scf "`t"
StringTrimRight, Table, Table, 1
Table .= ( _b ? "" : sv_s ) ( _d ? "" : sv_c ) ( _f ? "" : sv_f )
frow := LV_GetNext( 0, "F" ) ; look up the focused row's index
Loop, % LV_GetCount() ; loop through each row in the listview looking for eligible rows
If ( _e || A_Index = frow )
&& ( _c || A_Index = LV_GetNext( A_Index - 1, "C" ) )
&& ( _a || A_Index = LV_GetNext( A_Index - 1 ) )
{
row := A_Index
Loop, %ColCount%
If LV_GetText( scf, row, A_Index )
{
StringReplace, scf, scf, `n, , A
StringReplace, scf, scf, % "`t", 	, A
Table .= ( A_Index = 1 ? "`n" : "`t" ) scf
}
Table .= ( _b ? "" : ( row = LV_GetNext( row - 1 ) ? sv_s : "`t" ) )
. ( _d ? "" : ( row = LV_GetNext( row - 1, "C" ) ? sv_c : "`t" ) )
. ( _f ? "" : ( row = frow ? sv_f : "`t" ) )
}
Return Table, ErrorLevel := oel
} ; Table_FromListview( scf="" ) -------------------------------------------------------------------
Table_FromLvHWND( hwnd, What_Rows="all" ) { ; -----------------------------------------------------------------
; Extracts text from a listview (identified by its HWND) and returns it in table format. 'What_Rows' may be
; "header", "all", blank, "selected", "focused", "checked", or a list of row indices.
; Blank and "All" are synonymous and the result will be all of the listview's text (default option).
; "Seleced", "Focused", and "Checked" yield a table with those rows (abbreviations are OK).
; "Header" yields no row text. Row indices less than 1 are considered an offset from the last listview row.
Static u, w_a, ptr, psz, pid, lisz, DW := "UInt", bfsz = 8100, WM_ENABLE = 9, WM_SETREDRAW = 11
, PROCESS_VM_OPERATION = 8
, PROCESS_VM_READ = 16
, PROCESS_VM_WRITE = 32
, PROCESS_QUERY_INFORMATION = 1024
, LVM_GETITEMCOUNT = 0x1004
, LVM_GETNEXTITEM = 0x100C
, LVM_GETITEMSTATE = 0x102C
, LVM_GETITEMTEXT
, LVM_GETHEADER = 0x101F
, LVM_GETITEM = 0x104B
, HDM_GETITEMCOUNT = 0x1200
, HDM_GETITEM
, PAGE_READWRITE = 4
, MEM_COMMIT = 0x1000
, MEM_RESERVE = 0x2000
, MEM_RELEASE = 0x8000
oel := ErrorLevel
; Initialize static vars. send % fhex( 0x1000 + 44 )
If !psz
{
u := A_IsUnicode = 1
w_a := u ? "W" : "A"
ptr := A_PtrSize = "" ? DW : "Ptr"
psz := A_PtrSize = "" ? 4 : A_PtrSize
Process, Exist
pid := ErrorLevel
lisz := psz * 3 + 12 * 4
HDM_GETITEM := u ? 0x120B : 0x1203
LVM_GETITEMTEXT := u ? 0x1073 : 0x102D
}
VarSetCapacity( bufr, bfsz + 1 << u, 0 )
; Check the class name of the control. If it's not a listview, fail.
; Get the handle to listview's header and get the PID of its owner process
If !DllCall( "GetClassName" w_a, Ptr, hwnd, "Str", bufr, DW, bfsz ) || ( bufr != "SysListView32" )
|| !( hdrh := DllCall( "SendMessage", Ptr, hwnd, DW, LVM_GETHEADER, Ptr, 0, Ptr, 0 ) )
|| !DllCall( "GetWindowThreadProcessId", Ptr, hwnd, DW "*", pos := 0 )
Return "", ErrorLevel := oel ; Fail: it doesn't have a header or we can't get its owner PID
; Do some prep for both modes and determine which rows to get.
rowc := pmem := 0
If ( lvic := DllCall( "SendMessage", Ptr, hwnd, DW, LVM_GETITEMCOUNT, Ptr, 0, Ptr, 0 ) )
{
VarSetCapacity( rose, lvic << 2, 0 )
StringLower, What_Rows, What_Rows
If ( hdrc := InStr( "fs", SubStr( What_Rows "x", 1, 1 ) ) )
{
Loop
If ( pmem := 1 + DllCall( "SendMessage", Ptr, hwnd, DW, LVM_GETNEXTITEM, Ptr, pmem - 1, Ptr, hdrc, "Int" ) )
NumPut( pmem - 1, rose, 4 * ( rowc++ ), DW )
Else Break
}
Else If InStr( What_Rows, "c" ) = 1
{
Loop % lvic
If DllCall( "SendMessage", Ptr, hwnd, DW, LVM_GETITEMSTATE, Ptr, A_Index - 1, Ptr, 0xF000, DW ) != 0x1000
NumPut( A_Index - 1, rose, 4 * ( rowc++ ), DW )
}
Else If InStr( What_Rows, "a" ) = 1
{
Loop % rowc := lvic
NumPut( A_Index - 1, rose, A_Index - 1 << 2, DW )
}
Else If InStr( What_Rows, "h" ) != 1
{
Loop, Parse, What_Rows, % "`t`n`r !""#$%&'()*+,/:;<=>?@[\]^_`{|}~"
If A_LoopField IS NUMBER
If ( 0 < ( pmem := A_LoopField | 0 ) || 0 < ( pmem += lvic ) ) && ( pmem <= lvic )
NumPut( pmem - 1, rose, 4 * ( rowc++ ), DW )
}
}
hdrc := DllCall( "SendMessage", Ptr, hdrh, DW, HDM_GETITEMCOUNT, Ptr, 0, Ptr, 0 )
VarSetCapacity( hdtx, hdrc << 4 + u, 0 ) ; guesstimate 15 chars per column
VarSetCapacity( bdtx, hdrc * rowc << 5 + u, 0 ) ; guesstimate 31 chars per listview cell
; If the listview is owned by this very script, use our own memory to grab listview stuff
If ( pos = pid )
{
; Prep the HDITEM struct ( Mask = HDI_TEXT, pszText = &bufr, cchTextMax = bfsz )
VarSetCapacity( mitm, lisz, 0 )
NumPut( 2, mitm, 0, DW )
NumPut( &bufr, mitm, 8, Ptr )
NumPut( bfsz, mitm, 8 + 2 * psz, DW )
; Get the header
Loop % hdrc
If DllCall( "SendMessage", Ptr, hdrh, DW, HDM_GETITEM, Ptr, A_Index - 1, Ptr, &mitm )
VarSetCapacity( bufr, -1 ), hdtx .= bufr "`t"
; Now grab the listview text for each row saved in 'rose'
NumPut( &bufr, mitm, 16 + psz, Ptr )
NumPut( bfsz, mitm, 16 + 2 * psz, DW )
Loop % rowc
{
NumPut( pos := NumGet( rose, A_Index - 1 << 2, DW ), mitm, 4, DW )
Loop % hdrc
{
NumPut( A_Index - 1, mitm, 8, DW )
DllCall( "SendMessage", Ptr, hwnd, DW, LVM_GETITEMTEXT, Ptr, pos, Ptr, &mitm )
VarSetCapacity( bufr, -1 ), bdtx .= ( A_Index = 1 ? "`n" : "`t" ) bufr
}
}
}
; The princess is in another castle.... the listview is owned by another process, so use shared memory.
Else
{
lvic := PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE
; Try to open the process to which the listview belongs so we can allocate shared memory for getting the header text.
If !proc := DllCall( "OpenProcess", DW, lvic, DW, 0, DW, pos )
Return "", ErrorLevel := oel ; Fail
; Try to allocate shared memory. The local buffer size (8100) is meant to keep the remote buffer's size below 8K
If !pmem := DllCall( "VirtualAllocEx", ptr, proc, ptr, 0, DW, ( bfsz + 1 << u ) + lisz, DW, MEM_RESERVE | MEM_COMMIT, DW, PAGE_READWRITE )
Return "", DllCall( "CloseHandle", ptr, proc ), ErrorLevel := oel ; Fail
; Check whether the target process is 64 bit (or if the computer is even capable of telling the difference)
If ( lvic := DllCall( "GetProcAddress", Ptr, DllCall( "GetModuleHandle", Str, "Kernel32" ), Str, "IsWow64Process" ) )
DllCall( "IsWow64Process", Ptr, proc, DW "*", lvic )
lvic := lvic ? 8 : 4
bdtx := lvic = 4 ? DW : "Int64"
; Initialize the HDITEM structure in the remote memory space.
DllCall( "WriteProcessMemory", ptr, proc, ptr, pmem, DW "*", 2, DW, 4, ptr, 0 )
DllCall( "WriteProcessMemory", ptr, proc, ptr, pmem + 8 + lvic * 2, DW "*", bfsz, DW, 4, ptr, 0 )
; Grab the header text.
Loop % hdrc
{
DllCall( "WriteProcessMemory", ptr, proc, ptr, pmem + 8, bdtx "*", pmem + lisz, DW, lvic, ptr, 0 )
DllCall( "SendMessage", Ptr, hdrh, DW, HDM_GETITEM, Ptr, A_Index - 1, Ptr, pmem )
DllCall( "ReadProcessMemory", ptr, proc, ptr, pmem + 8, ptr "*", pos, DW, lvic, ptr, 0 )