Skip to content

Commit 7a0f828

Browse files
committed
Add method to wrap list of maps as a table
This will be useful in script languages like Groovy, so that (small) tables can be written succinctly: table = Tables.wrap([ ["Town": "Shanghai", "Population": 24_256_800], ["Town": "Karachi", "Population": 23_500_000], ["Town": "Beijing", "Population": 21_516_000], ["Town": "Sao Paolo", "Population": 21_292_893], ])
1 parent 948fe5f commit 7a0f828

File tree

2 files changed

+359
-0
lines changed

2 files changed

+359
-0
lines changed

src/main/java/org/scijava/table/Tables.java

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030

3131
package org.scijava.table;
3232

33+
import java.util.Collection;
34+
import java.util.List;
35+
import java.util.Map;
36+
import java.util.Objects;
37+
3338
/**
3439
* Utility methods for constructing tables.
3540
*
@@ -41,6 +46,99 @@ private Tables() {
4146
// NB: Prevent instantiation of utility class.
4247
}
4348

49+
/**
50+
* Creates a table wrapping a list of maps. Each map is one row of the table.
51+
* Map keys are column names; map values are cell data.
52+
* <p>
53+
* Note that columns are inferred from the first map/row of the table, not
54+
* unioned across all rows.
55+
* </p>
56+
*
57+
* @param data The data to wrap. Each list element is a row; maps go from
58+
* column name to data.
59+
* @param rowHeaders List of row header labels. Pass null for no row headers.
60+
* @return A {@link Table} object wrapping the data structure.
61+
*/
62+
public static <T> Table<Column<T>, T> wrap(
63+
final List<? extends Map<?, ? extends T>> data, final List<String> rowHeaders)
64+
{
65+
if (data.isEmpty()) throw new IllegalArgumentException("Cannot wrap an empty list");
66+
67+
return new ReadOnlyTable<T>() {
68+
69+
@Override
70+
public int getRowCount() {
71+
return data.size();
72+
}
73+
74+
@Override
75+
public String getRowHeader(final int row) {
76+
if (rowHeaders == null || rowHeaders.size() < row) return null;
77+
return rowHeaders.get(row);
78+
}
79+
80+
@Override
81+
public int size() {
82+
return data.get(0).size();
83+
}
84+
85+
@Override
86+
public Column<T> get(final int col) {
87+
class ColumnAccessor implements ReadOnlyColumn<T> {
88+
89+
private final Object tableData = data;
90+
private boolean initialized;
91+
private String colHeader;
92+
93+
@Override
94+
public String getHeader() {
95+
if (!initialized) {
96+
int c = 0;
97+
for (final Object key : data.get(0).keySet()) {
98+
if (col == c++) {
99+
colHeader = key == null ? "<null>" : key.toString();
100+
break;
101+
}
102+
}
103+
initialized = true;
104+
}
105+
return colHeader;
106+
}
107+
108+
@Override
109+
public int size() {
110+
return data.size();
111+
}
112+
113+
@Override
114+
public Class<T> getType() {
115+
// TODO: Consider whether this is terrible.
116+
throw new UnsupportedOperationException();
117+
}
118+
119+
@Override
120+
public T get(final int index) {
121+
return data.get(index).get(getHeader());
122+
}
123+
124+
@Override
125+
public int hashCode() {
126+
return getHeader().hashCode();
127+
}
128+
129+
@Override
130+
public boolean equals(final Object obj) {
131+
if (!(obj instanceof ColumnAccessor)) return false;
132+
final ColumnAccessor other = ((ColumnAccessor) obj);
133+
if (data != other.tableData) return false;
134+
return Objects.equals(getHeader(), other.getHeader());
135+
}
136+
}
137+
return new ColumnAccessor();
138+
}
139+
};
140+
}
141+
44142
// -- Internal methods --
45143

46144
/**
@@ -98,4 +196,158 @@ private static void check(final String name, final int index, final int count,
98196
throw new IndexOutOfBoundsException("Invalid " + name + "s: " + index +
99197
" - " + last);
100198
}
199+
200+
private static UnsupportedOperationException readOnlyException() {
201+
return new UnsupportedOperationException("Wrapped table is read-only");
202+
}
203+
204+
// -- Helper classes --
205+
206+
/** Read-only version of a {@link Table}. */
207+
private static interface ReadOnlyTable<T> extends Table<Column<T>, T> {
208+
209+
@Override
210+
default void setColumnCount(final int colCount) {
211+
throw readOnlyException();
212+
}
213+
214+
@Override
215+
default List<Column<T>> insertColumns(final int col, final int count) {
216+
throw readOnlyException();
217+
}
218+
219+
@Override
220+
default void setRowCount(final int rowCount) {
221+
throw readOnlyException();
222+
}
223+
224+
@Override
225+
default void removeRows(int row, int count) {
226+
throw readOnlyException();
227+
}
228+
229+
@Override
230+
default void setRowHeader(int row, String header) {
231+
throw readOnlyException();
232+
}
233+
234+
@Override
235+
default void set(String colHeader, int row, T value) {
236+
throw readOnlyException();
237+
}
238+
239+
@Override
240+
default boolean add(final Column<T> column) {
241+
throw readOnlyException();
242+
}
243+
244+
@Override
245+
default boolean remove(final Object column) {
246+
throw readOnlyException();
247+
}
248+
249+
@Override
250+
default boolean addAll(Collection<? extends Column<T>> c) {
251+
throw readOnlyException();
252+
}
253+
254+
@Override
255+
default boolean addAll(int col, Collection<? extends Column<T>> c) {
256+
throw readOnlyException();
257+
}
258+
259+
@Override
260+
default boolean removeAll(Collection<?> c) {
261+
throw readOnlyException();
262+
}
263+
264+
@Override
265+
default boolean retainAll(Collection<?> c) {
266+
throw readOnlyException();
267+
}
268+
269+
@Override
270+
default void clear() {
271+
throw readOnlyException();
272+
}
273+
274+
@Override
275+
default Column<T> set(int col, Column<T> column) {
276+
throw readOnlyException();
277+
}
278+
279+
@Override
280+
default void add(int col, Column<T> column) {
281+
throw readOnlyException();
282+
}
283+
284+
@Override
285+
default Column<T> remove(int col) {
286+
throw readOnlyException();
287+
}
288+
}
289+
290+
/** Read-only version of a {@link Column}. */
291+
private static interface ReadOnlyColumn<T> extends Column<T> {
292+
293+
@Override
294+
default boolean add(final T e) {
295+
throw readOnlyException();
296+
}
297+
298+
@Override
299+
default boolean remove(Object o) {
300+
throw readOnlyException();
301+
}
302+
303+
@Override
304+
default boolean addAll(Collection<? extends T> c) {
305+
throw readOnlyException();
306+
}
307+
308+
@Override
309+
default boolean addAll(int index, Collection<? extends T> c) {
310+
throw readOnlyException();
311+
}
312+
313+
@Override
314+
default boolean removeAll(Collection<?> c) {
315+
throw readOnlyException();
316+
}
317+
318+
@Override
319+
default boolean retainAll(Collection<?> c) {
320+
throw readOnlyException();
321+
}
322+
323+
@Override
324+
default void clear() {
325+
throw readOnlyException();
326+
}
327+
328+
@Override
329+
default T set(int index, T element) {
330+
throw readOnlyException();
331+
}
332+
333+
@Override
334+
default void add(int index, T element) {
335+
throw readOnlyException();
336+
}
337+
338+
@Override
339+
default T remove(int index) {
340+
throw readOnlyException();
341+
}
342+
343+
@Override
344+
default void setHeader(String header) {
345+
throw readOnlyException();
346+
}
347+
348+
@Override
349+
default void setSize(int size) {
350+
throw readOnlyException();
351+
}
352+
}
101353
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* #%L
3+
* Table structures for SciJava.
4+
* %%
5+
* Copyright (C) 2012 - 2018 Board of Regents of the University of
6+
* Wisconsin-Madison, and Friedrich Miescher Institute for Biomedical Research.
7+
* %%
8+
* Redistribution and use in source and binary forms, with or without
9+
* modification, are permitted provided that the following conditions are met:
10+
*
11+
* 1. Redistributions of source code must retain the above copyright notice,
12+
* this list of conditions and the following disclaimer.
13+
* 2. Redistributions in binary form must reproduce the above copyright notice,
14+
* this list of conditions and the following disclaimer in the documentation
15+
* and/or other materials provided with the distribution.
16+
*
17+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
21+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
* POSSIBILITY OF SUCH DAMAGE.
28+
* #L%
29+
*/
30+
31+
package org.scijava.table;
32+
33+
import static org.junit.Assert.assertEquals;
34+
35+
import java.util.Arrays;
36+
import java.util.LinkedHashMap;
37+
import java.util.List;
38+
import java.util.Map;
39+
40+
import org.junit.Test;
41+
42+
/**
43+
* Tests {@link Tables}.
44+
*
45+
* @author Curtis Rueden
46+
*/
47+
public class TablesTest {
48+
49+
@Test
50+
public void testWrap() {
51+
final List<Map<Object, Object>> towns = Arrays.asList( //
52+
map("Town", "Shanghai", "Population", 24_256_800), //
53+
map("Town", "Karachi", "Population", 23_500_000), //
54+
map("Town", "Beijing", "Population", 21_516_000), //
55+
map("Town", "Sao Paolo", "Population", 21_292_893));
56+
57+
final List<String> rowHeaders = Arrays.asList("A", "B", "C", "D");
58+
59+
final Table<Column<Object>, Object> table = Tables.wrap(towns, rowHeaders);
60+
61+
// check table dimensions
62+
assertEquals(2, table.size());
63+
assertEquals(2, table.getColumnCount());
64+
assertEquals(4, table.getRowCount());
65+
66+
// check row headers
67+
for (int r = 0; r < rowHeaders.size(); r++)
68+
assertEquals(rowHeaders.get(r), table.getRowHeader(r));
69+
70+
// check direct data access
71+
assertEquals("Shanghai", table.get(0, 0));
72+
assertEquals("Karachi", table.get(0, 1));
73+
assertEquals("Beijing", table.get(0, 2));
74+
assertEquals("Sao Paolo", table.get(0, 3));
75+
assertEquals(24_256_800, table.get(1, 0));
76+
assertEquals(23_500_000, table.get(1, 1));
77+
assertEquals(21_516_000, table.get(1, 2));
78+
assertEquals(21_292_893, table.get(1, 3));
79+
80+
// check first column
81+
assertEquals("Town", table.getColumnHeader(0));
82+
final Column<Object> townColumn = table.get("Town");
83+
assertEquals(4, townColumn.size());
84+
assertEquals("Shanghai", townColumn.get(0));
85+
assertEquals("Karachi", townColumn.get(1));
86+
assertEquals("Beijing", townColumn.get(2));
87+
assertEquals("Sao Paolo", townColumn.get(3));
88+
assertEquals(townColumn, table.get(0));
89+
90+
// check second column
91+
assertEquals("Population", table.getColumnHeader(1));
92+
final Column<Object> popColumn = table.get("Population");
93+
assertEquals(4, popColumn.size());
94+
assertEquals(24_256_800, popColumn.get(0));
95+
assertEquals(23_500_000, popColumn.get(1));
96+
assertEquals(21_516_000, popColumn.get(2));
97+
assertEquals(21_292_893, popColumn.get(3));
98+
assertEquals(popColumn, table.get(1));
99+
}
100+
101+
private Map<Object, Object> map(final Object... kv) {
102+
final LinkedHashMap<Object, Object> map = new LinkedHashMap<>();
103+
for (int i = 0; i < kv.length; i += 2)
104+
map.put(kv[i], kv[i + 1]);
105+
return map;
106+
}
107+
}

0 commit comments

Comments
 (0)