-
Notifications
You must be signed in to change notification settings - Fork 23
/
Tables.java
567 lines (531 loc) · 21.5 KB
/
Tables.java
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
package uk.ac.starlink.table;
import java.io.IOException;
import java.lang.reflect.Array;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import uk.ac.starlink.table.jdbc.JDBCStarTable;
/**
* Utility class for miscellaneous table-related functionality.
*/
public class Tables {
/**
* ValueInfo which may be used as part of a column's metadata to indicate
* a special value (preferably a {@link java.lang.Number}
* that should be interpreted as a null (blank).
* This should only be used on nullable columns, and really only
* on ones with a contentClass which is an integer (or possibly boolean)
* type; for other types, there is usually a value which can
* conventionally be understood to mean blank.
* Note this is here as a standard key to use when software components
* wish to communicate this information; the table system does not
* guarantee to honour instances of this value in a column's
* auxiliary data. It is the job of a StarTable instance to ensure
* that a <tt>null</tt> is returned from the table interrogation methods
* if that is what is meant.
*/
public static final ValueInfo NULL_VALUE_INFO =
new DefaultValueInfo( "NULL_VALUE", Object.class,
"Integer value which represents a null" );
/**
* ValueInfo representing Right Ascension.
* The units are radians and it is non-nullable.
*/
public static final DefaultValueInfo RA_INFO =
new DefaultValueInfo( "RA", Number.class, "Right Ascension" );
/**
* ValueInfo representing Declination.
* The units are radians and it is non-nullable.
*/
public static final DefaultValueInfo DEC_INFO =
new DefaultValueInfo( "Dec", Number.class, "Declination" );
static {
RA_INFO.setUnitString( "radians" );
DEC_INFO.setUnitString( "radians" );
RA_INFO.setNullable( false );
DEC_INFO.setNullable( false );
RA_INFO.setUCD( "POS_EQ_RA" );
DEC_INFO.setUCD( "POS_EQ_DEC" );
}
/**
* Returns a table based on a given table and guaranteed to have
* random access. If the original table <tt>stab</tt> has random
* access then it is returned, otherwise a new random access table
* is built using its data.
*
* <p>This convenience method is equivalent to calling
* <tt>StoragePolicy.getDefaultPolicy().randomTable(startab)</tt>.
*
* @param startab original table
* @return a table with the same data as <tt>startab</tt> and with
* <tt>isRandom()==true</tt>
*/
public static StarTable randomTable( StarTable startab )
throws IOException {
return StoragePolicy.getDefaultPolicy().randomTable( startab );
}
/**
* Convenience method to return an array of all the column headers
* in a given table. Modifying this array will not affect the table.
*
* @param startab the table being enquired about
* @return an array of all the column headers
*/
public static ColumnInfo[] getColumnInfos( StarTable startab ) {
int ncol = startab.getColumnCount();
ColumnInfo[] infos = new ColumnInfo[ ncol ];
for ( int i = 0; i < ncol; i++ ) {
infos[ i ] = startab.getColumnInfo( i );
}
return infos;
}
/**
* Returns a table equivalent to the original but with a given column
* deleted. The result may or may not be the same object as the
* input table.
*
* @param startab the table from which to delete a column
* @param icol the index of the column to be deleted
* @throws IndexOutOfBoundsException if <tt>startab</tt> has no column
* at <tt>icol</tt>
*/
public static StarTable deleteColumn( StarTable startab, int icol ) {
int ncol = startab.getColumnCount();
if ( icol < 0 || icol >= ncol ) {
throw new IndexOutOfBoundsException(
"Deleted column " + icol + " out of range 0.." + ( ncol - 1 ) );
}
int[] colmap = new int[ ncol - 1 ];
int j = 0;
for ( int i = 0; i < ncol; i++ ) {
if ( i != icol ) {
colmap[ j++ ] = i;
}
}
assert j == ncol - 1;
return new ColumnPermutedStarTable( startab, colmap );
}
/**
* Copies the data and metadata from a <tt>StarTable</tt> into a
* table sink.
* This method is supplied for convenience; its implementation is
* very straightforward.
*
* @param source table to be copied
* @param sink table destination
*/
public static void streamStarTable( StarTable source, TableSink sink )
throws IOException {
sink.acceptMetadata( source );
RowSequence rseq = source.getRowSequence();
try {
while ( rseq.next() ) {
sink.acceptRow( rseq.getRow() );
}
}
finally {
rseq.close();
}
sink.endRows();
}
/**
* Diagnostic method which tests the invariants of a StarTable.
* This method returns no value, and throws an exception if a table
* is illegal in some way.
* If this method throws an exception when called on a given StarTable,
* that table is deemed to be bad in some way (buggy implementation
* or I/O trouble during access).
* This method is provided for convenience and diagnostics
* because there are a number of ways, some of them subtle, in which
* a StarTable can fail to fulfil its general contract.
* <p>
* That a table passes this test does not guarantee that the table has
* no bugs. This method should not generally be used in production
* code, since it may be expensive in time and/or memory.
*
* @param table table to test
* @throws AssertionError if an invariant is violated
* @throws IOException if there is an I/O error
*/
public static void checkTable( StarTable table ) throws IOException {
int formatChars = 100;
int ncol = table.getColumnCount();
long nrow = table.getRowCount();
boolean isRandom = table.isRandom();
/* Check a random-access table knows how many rows it has. */
if ( isRandom ) {
assertTrue( nrow >= 0 );
}
/* Check the shape arrays look OK. */
int[] nels = new int[ ncol ];
Class[] classes = new Class[ ncol ];
ColumnInfo[] colinfos = new ColumnInfo[ ncol ];
for ( int icol = 0; icol < ncol; icol++ ) {
ColumnInfo cinfo = table.getColumnInfo( icol );
colinfos[ icol ] = cinfo;
classes[ icol ] = cinfo.getContentClass();
int[] dims = cinfo.getShape();
if ( dims != null ) {
int ndim = dims.length;
assertTrue( ndim > 0 );
assertTrue( cinfo.getContentClass().isArray() );
int nel = 1;
for ( int i = 0; i < ndim; i++ ) {
nel *= dims[ i ];
assertTrue( dims[ i ] != 0 );
if ( i < ndim - 1 ) {
assertTrue( dims[ i ] > 0 );
}
}
nels[ icol ] = nel;
}
assertTrue( cinfo.getContentClass().isArray() == cinfo.isArray() );
}
/* Get a RowSequence object and check it can't be read before a next. */
RowSequence rseq = table.getRowSequence();
try {
rseq.getRow();
assertTrue( false );
}
catch ( IllegalStateException e ) {
// ok
}
/* Read all cells. */
long lrow = 0L;
while ( rseq.next() ) {
Object[] row = rseq.getRow();
for ( int icol = 0; icol < ncol; icol++ ) {
/* Check that elements got in different ways look the same. */
Object cell = row[ icol ];
Object val1 = cell;
Object val2 = rseq.getCell( icol );
Object val3 = null;
if ( isRandom ) {
val3 = table.getCell( lrow, icol );
}
boolean isNull = cell == null;
if ( isNull ) {
assertTrue( colinfos[ icol ].isNullable() );
assertTrue( val2 == null );
if ( isRandom ) {
assertTrue( val3 == null);
}
}
else {
String s1 = colinfos[ icol ]
.formatValue( val1, formatChars );
assertTrue( s1.equals( colinfos[ icol ]
.formatValue( val2, formatChars ) ) );
if ( isRandom ) {
assertTrue( s1.equals( colinfos[ icol ]
.formatValue( val3,
formatChars ) ) );
}
}
/* If the cell is an array, check it's the right shape. */
if ( cell != null && cell.getClass().isArray() ) {
int nel = Array.getLength( cell );
if ( nels[ icol ] < 0 ) {
assertTrue( nel % nels[ icol ] == 0 );
}
else {
assertTrue( nels[ icol ] == nel );
}
}
/* Check the cell is of the declared type. */
if ( cell != null ) {
if ( ! classes[ icol ]
.isAssignableFrom( cell.getClass() ) ) {
throw new AssertionError( "Column " + ( icol + 1 ) +
": " + cell + " is a " + cell.getClass().getName() +
" not a " + classes[ icol ].getName() );
}
}
}
lrow++;
}
rseq.close();
/* Check that the claimed number of rows is correct. */
if ( nrow >= 0 ) {
assertTrue( lrow == nrow );
}
}
/**
* Indicates whether a given value is conventionally regarded as a
* blank value. For most objects this is equivalent to testing
* whether it is equall to <tt>null</tt>, but some classes have
* additional non-<tt>null</tt> values which count as blanks,
* for instance zero-length Strings and floating Not-A-Number values.
*
* @param value value to test
* @return true iff <tt>value</tt> counts as a blank value
*/
public static boolean isBlank( Object value ) {
return ( value == null )
|| ( value instanceof Float && ((Float) value).isNaN() )
|| ( value instanceof Double && ((Double) value).isNaN() )
|| ( value instanceof String && ((String) value).length() == 0 )
|| ( value.getClass().isArray() && Array.getLength( value ) == 0 )
|| false;
}
/**
* Convenience method to consruct a TableSequence for a single table.
*
* @param table table
* @return table sequence with just one element
*/
public static TableSequence singleTableSequence( StarTable table ) {
return arrayTableSequence( new StarTable[] { table } );
}
/**
* Convenience method to construct a TableSequence for a supplied array
* of tables.
*
* @param tables table array
* @return table sequence containing input tables
*/
public static TableSequence arrayTableSequence( StarTable[] tables ) {
final Iterator it = Arrays.asList( tables ).iterator();
return new TableSequence() {
public StarTable nextTable() {
return it.hasNext() ? (StarTable) it.next()
: null;
}
};
}
/**
* Convenience method to construct an array of StarTables from a
* TableSequence.
*
* @param tseq table sequence
* @return array containing tables from sequence
*/
public static StarTable[] tableArray( TableSequence tseq )
throws IOException {
List<StarTable> list = new ArrayList<StarTable>();
for ( StarTable table; ( table = tseq.nextTable() ) != null; ) {
list.add( table );
}
return list.toArray( new StarTable[ 0 ] );
}
/**
* Returns a sorted version of a table. The sorting is done on the
* values of one or more columns, as specified by the <tt>colIndices</tt>
* argument; the first element is the primary sort key, but in case
* of a tie the second element is used, and so on. The original table
* is not affected. The natural comparison order of the values in
* the table is used, and blank values may be ranked at the start or
* end of the collation order.
*
* @param table table to sort - must be random access
* @param colIndices indices of the columns which are to act as sort
* keys; first element is primary key etc
* @param up true for sorting into ascending order, false for
* descending order
* @param nullsLast true if blank values should be considered
* last in the collation order, false if they should
* be considered first
* @return a table with the same rows as <tt>table</tt> but in an
* order determined by the other arguments
* @throws IOException if <tt>table.isRandom</tt> is not true
*/
public static StarTable sortTable( StarTable table, int[] colIndices,
boolean up, boolean nullsLast )
throws IOException {
long[] rowMap =
TableSorter.getSortedOrder( table, colIndices, up, nullsLast );
return new RowPermutedStarTable( table, rowMap );
}
/**
* Convenience method to get an <tt>int</tt> value from a <tt>long</tt>.
* If the supplied long integer <tt>lval</tt> is out of the range
* which can be represented in an <tt>int</tt>, then unlike a
* typecast, this method will throw an <tt>IllegalArgumentException</tt>.
*
* @param lval the <tt>long</tt> value to convert
* @return an <tt>int</tt> value which has the same value as <tt>lval</tt>
* @throws IllegalArgumentException if the conversion cannot be done
*/
public static int checkedLongToInt( long lval ) {
int ival = (int) lval;
if ( (long) ival == lval ) {
return ival;
}
else {
if ( ival < Integer.MIN_VALUE ) {
throw new IllegalArgumentException( "Out of supported range: "
+ ival + " < Integer.MIN_VALUE" );
}
else if ( ival > Integer.MAX_VALUE ) {
throw new IllegalArgumentException( "Out of supported range: "
+ ival + " > Integer.MAX_VALUE" );
}
else {
throw new AssertionError();
}
}
}
/**
* Casts a long to an int, with an assertion that no truncation occurs.
*
* @param lval long value, asserted to be in the range
* Integer.MIN_VALUE..Integer.MAX_VALUE
* @return truncated version of <code>lval</code>
*/
public static int assertLongToInt( long lval ) {
int ival = (int) lval;
assert ival == lval
: "Long value " + lval + " unexpectedly out of int range";
return ival;
}
/**
* Returns an array of strings suitable as labels or label suffixes
* for elements of an array as returned by {@link ValueInfo#getShape}.
* If the given <tt>shape</tt> cannot be decomposed into a fixed
* size array, returns <tt>null</tt>.
*
* @param shape vector giving dimensions of an array value
* @return array with one element for each element of the array values
* (in their natural order), or null for non-array shapes
*/
public static String[] getElementLabels( int[] shape ) {
if ( shape == null || shape.length == 0 ) {
return null;
}
for ( int i = 0; i < shape.length; i++ ) {
if ( shape[ i ] <= 0 ) {
return null;
}
}
List labels = new ArrayList();
for ( Iterator it = new ShapeIterator( shape ); it.hasNext(); ) {
int[] pos = (int[]) it.next();
StringBuffer sbuf = new StringBuffer();
for ( int i = 0; i < pos.length; i++ ) {
sbuf.append( '_' )
.append( Integer.toString( pos[ i ] + 1 ) );
}
labels.add( sbuf.toString() );
}
return (String[]) labels.toArray( new String[ 0 ] );
}
/**
* Returns the Utype associated with a given metadata item.
*
* @deprecated use {@link ValueInfo#getUtype()} instead
* @param info metadata item
* @return utype string, or null if none is available
* @see #setUtype
*/
public static String getUtype( ValueInfo info ) {
return info.getUtype();
}
/**
* Tries to set the Utype for a given metadata item.
*
* @deprecated use {@link DefaultValueInfo#setUtype} instead
* @param info metadata item
* @param utype new utype value
* @see #getUtype
*/
public static void setUtype( ValueInfo info, String utype ) {
if ( info instanceof DefaultValueInfo ) {
((DefaultValueInfo) info).setUtype( utype );
}
}
/**
* Returns the value from a list of {@link DescribedValue} objects
* which corresponds to a given info key.
* If the key is not represented in the list, or if its value is null,
* then null is returned.
*
* @param dvals list of DescribedValue objects
* @param info key giving the value you want
* @return matching value
*/
public static Object getValue( Collection dvals, ValueInfo info ) {
String iname = info.getName();
Class iclazz = info.getContentClass();
if ( iname != null && iclazz != null ) {
for ( Iterator it = dvals.iterator(); it.hasNext(); ) {
Object obj = it.next();
if ( obj instanceof DescribedValue ) {
DescribedValue dval = (DescribedValue) obj;
ValueInfo dinfo = dval.getInfo();
if ( iname.equals( dinfo.getName() ) &&
iclazz.equals( dinfo.getContentClass() ) ) {
return dval.getValue();
}
}
}
}
return null;
}
/**
* Performs deduplication of column names for N lists of column metadata
* objects that will be combined to form a new table.
* The arguments are matched arrays; each list of column infos
* has a corresponding renaming policy.
* The ColumnInfo objects are renamed in place as required.
*
* @param infoLists array of N arrays of column metadata objects
* @param fixActs array of N policies for renaming columns
*/
public static void fixColumns( ColumnInfo[][] infoLists,
JoinFixAction[] fixActs ) {
/* Check there is a 1:1 correspondence of infoLists and fix policies. */
int nt = infoLists.length;
if ( fixActs.length != nt ) {
throw new IllegalArgumentException();
}
/* Find the longest list of columns. This is just used so that we
* can get a unique numeric index for any column iCol of table iTab. */
int maxNc = 0;
for ( int it = 0; it < nt; it++ ) {
maxNc = Math.max( infoLists[ it ].length, maxNc );
}
/* Put all the infos into a single List. */
String[] names = new String[ maxNc * nt ];
for ( int it = 0; it < nt; it++ ) {
ColumnInfo[] infos = infoLists[ it ];
for ( int ic = 0; ic < infos.length; ic++ ) {
int ix = it * maxNc + ic;
names[ ix ] = infos[ ic ].getName();
}
}
List<String> nameList = new ArrayList<String>( Arrays.asList( names ) );
/* For each info, deduplicate its name as required in the context
* of all the others. */
for ( int it = 0; it < nt; it++ ) {
ColumnInfo[] infos = infoLists[ it ];
JoinFixAction fixAct = fixActs[ it ];
for ( int ic = 0; ic < infos.length; ic++ ) {
int ix = it * maxNc + ic;
String origName = nameList.remove( ix );
assert origName.equals( infos[ ic ].getName() );
String name = fixAct.getFixedName( origName, nameList );
nameList.add( ix, origName );
if ( ! name.equals( origName ) ) {
nameList.add( name );
infos[ ic ].setName( name );
}
}
}
}
/**
* Implements assertion semantics. This differs from the <tt>assert</tt>
* Java 1.4 language element in that the assertion is always done,
* it doesn't depend on the JVM running in assertions-enabled mode.
*
* @param ok the thing that should be true
* @throws AssertionError if <tt>ok</tt> is <tt>false</tt>
*/
private static void assertTrue( boolean ok ) {
if ( ! ok ) {
throw new AssertionError();
}
}
}