use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(feature = "parallel_proc")]
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator};
use super::field_array::FieldArray;
#[cfg(feature = "views")]
use crate::TableV;
#[cfg(feature = "views")]
use crate::aliases::CubeV;
use crate::enums::{error::MinarrowError, shape_dim::ShapeDim};
use crate::ffi::arrow_dtype::ArrowType;
use crate::traits::{concatenate::Concatenate, shape::Shape};
use crate::{Field, Table};
static UNNAMED_COUNTER: AtomicUsize = AtomicUsize::new(1);
#[repr(C, align(64))]
#[derive(Default, PartialEq, Clone, Debug)]
pub struct Cube {
pub tables: Vec<Arc<Table>>,
pub name: String,
pub third_dim_index: Option<Vec<String>>,
pub resolver: HashMap<String, usize>,
}
impl Cube {
pub fn new(
name: String,
cols: Option<Vec<FieldArray>>,
third_dim_index: Option<Vec<String>>,
) -> Self {
let name = if name.trim().is_empty() {
let id = UNNAMED_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("UnnamedCube{}", id)
} else {
name
};
let mut tables = Vec::new();
let mut resolver = HashMap::new();
if let Some(cols) = cols {
let table = Table::new(name.clone(), Some(cols));
resolver.insert(table.name.clone(), 0);
tables.push(Arc::new(table));
}
let cube = Self {
tables,
name,
third_dim_index,
resolver,
};
cube.validate_third_dim_index();
cube
}
pub fn from_tables(tables: Vec<Table>, name: String, index_by: Option<Vec<String>>) -> Self {
let arc_tables: Vec<Arc<Table>> = tables.into_iter().map(Arc::new).collect();
let mut resolver = HashMap::new();
for (i, t) in arc_tables.iter().enumerate() {
resolver.insert(t.name.clone(), i);
}
Self { tables: arc_tables, name, third_dim_index: index_by, resolver }
}
pub fn new_empty() -> Self {
let id = UNNAMED_COUNTER.fetch_add(1, Ordering::Relaxed);
let name = format!("UnnamedCube{}", id);
Self {
tables: Vec::new(),
name,
third_dim_index: None,
resolver: HashMap::new(),
}
}
pub fn add_table(&mut self, table: Table) {
if !self.tables.is_empty() {
let existing_fields: HashMap<String, ArrowType> = self.tables[0]
.cols()
.iter()
.map(|col| (col.field.name.clone(), col.field.dtype.clone()))
.collect();
for col in table.cols() {
let field = &col.field;
match existing_fields.get(&field.name) {
Some(existing_dtype) => assert_eq!(
existing_dtype, &field.dtype,
"Error: Schema mismatch between existing and new tables for Cube."
),
None => panic!(
"New table has field '{}' with datatype '{}' not present in existing tables.",
field.name, field.dtype
),
}
}
}
let idx = self.tables.len();
self.resolver.insert(table.name.clone(), idx);
self.tables.push(Arc::new(table));
}
pub fn schema(&self) -> Vec<Arc<Field>> {
self.tables[0].schema()
}
pub fn n_tables(&self) -> usize {
self.tables.len()
}
pub fn len(&self) -> usize {
self.n_tables()
}
pub fn n_rows(&self) -> Vec<usize> {
self.tables.iter().map(|t| t.n_rows()).collect()
}
pub fn n_cols(&self) -> usize {
self.tables[0].n_cols()
}
pub fn is_empty(&self) -> bool {
self.n_tables() == 0 || self.n_cols() == 0 || self.tables.iter().all(|t| t.n_rows() == 0)
}
pub fn table(&self, idx: usize) -> Option<&Arc<Table>> {
self.tables.get(idx)
}
pub fn table_mut(&mut self, idx: usize) -> Option<&mut Table> {
self.tables.get_mut(idx).map(|arc| Arc::make_mut(arc))
}
pub fn table_names(&self) -> Vec<&str> {
self.tables.iter().map(|t| t.name.as_str()).collect()
}
pub fn table_index(&self, name: &str) -> Option<usize> {
self.tables.iter().position(|t| t.name == name)
}
pub fn has_table(&self, name: &str) -> bool {
self.table_index(name).is_some()
}
pub fn remove_table_at(&mut self, idx: usize) -> bool {
if idx < self.tables.len() {
let removed = self.tables.remove(idx);
self.resolver.remove(&removed.name);
for (_, pos) in self.resolver.iter_mut() {
if *pos > idx {
*pos -= 1;
}
}
true
} else {
false
}
}
pub fn remove_table(&mut self, name: &str) -> bool {
if let Some(idx) = self.resolver.remove(name) {
self.tables.remove(idx);
for (_, pos) in self.resolver.iter_mut() {
if *pos > idx {
*pos -= 1;
}
}
true
} else {
false
}
}
pub fn clear(&mut self) {
self.tables.clear();
self.resolver.clear();
self.third_dim_index = None;
}
pub fn tables(&self) -> &[Arc<Table>] {
&self.tables
}
pub fn tables_mut(&mut self) -> &mut Vec<Arc<Table>> {
&mut self.tables
}
#[inline]
pub fn iter_tables(&self) -> std::slice::Iter<'_, Arc<Table>> {
self.tables.iter()
}
#[inline]
pub fn iter_tables_mut(&mut self) -> std::slice::IterMut<'_, Arc<Table>> {
self.tables.iter_mut()
}
pub fn col_names(&self) -> Vec<&str> {
if self.tables.is_empty() {
Vec::new()
} else {
self.tables[0].col_names()
}
}
pub fn col_name_index(&self, name: &str) -> Option<usize> {
if self.tables.is_empty() {
None
} else {
self.tables[0].col_name_index(name)
}
}
pub fn has_col(&self, name: &str) -> bool {
self.tables.iter().all(|t| t.has_col(name))
}
pub fn col_ix(&self, idx: usize) -> Option<Vec<&FieldArray>> {
if self.tables.is_empty() || idx >= self.n_cols() {
None
} else {
Some(
self.tables
.iter()
.filter_map(|t| t.cols().get(idx))
.collect(),
)
}
}
pub fn col_by_name(&self, name: &str) -> Option<Vec<&FieldArray>> {
if !self.has_col(name) {
None
} else {
Some(
self.tables
.iter()
.filter_map(|t| t.col_name_index(name).and_then(|idx| t.cols().get(idx)))
.collect(),
)
}
}
pub fn col_vec(&self) -> Vec<Vec<&FieldArray>> {
self.tables
.iter()
.map(|t| t.cols().iter().collect())
.collect()
}
pub fn remove_col(&mut self, name: &str) -> bool {
let mut all_removed = true;
for t in &mut self.tables {
if !Arc::make_mut(t).remove_col(name) {
all_removed = false;
}
}
all_removed
}
pub fn remove_col_at(&mut self, idx: usize) -> bool {
let mut all_removed = true;
for t in &mut self.tables {
if !Arc::make_mut(t).remove_col_at(idx) {
all_removed = false;
}
}
all_removed
}
pub fn resolve(&self, key: &str) -> Option<usize> {
self.resolver.get(key).copied()
}
pub fn rebuild_resolver(&mut self) {
self.resolver.clear();
for (i, t) in self.tables.iter().enumerate() {
self.resolver.insert(t.name.clone(), i);
}
}
#[inline]
pub fn iter_cols(&self, col_idx: usize) -> Option<impl Iterator<Item = &FieldArray>> {
if col_idx < self.n_cols() {
Some(
self.tables
.iter()
.filter_map(move |t| t.cols().get(col_idx)),
)
} else {
None
}
}
#[inline]
pub fn iter_cols_by_name<'a>(
&'a self,
name: &'a str,
) -> Option<impl Iterator<Item = &'a FieldArray> + 'a> {
if self.has_col(name) {
Some(
self.tables
.iter()
.filter_map(move |t| t.col_name_index(name).and_then(|idx| t.cols().get(idx))),
)
} else {
None
}
}
pub fn set_third_dim_index<S: Into<String>>(&mut self, cols: Vec<S>) {
self.third_dim_index = Some(cols.into_iter().map(|s| s.into()).collect());
}
pub fn third_dim_index(&self) -> Option<&[String]> {
self.third_dim_index.as_deref()
}
fn validate_third_dim_index(&self) {
if let Some(ref indices) = self.third_dim_index {
for col_name in indices {
assert!(
self.has_col(col_name),
"Index column '{}' not found in all tables",
col_name
);
}
}
}
#[cfg(feature = "views")]
pub fn slice_clone(&self, offset: usize, len: usize) -> Self {
assert!(!self.tables.is_empty(), "No tables to slice");
for t in &self.tables {
assert!(
offset + len <= t.n_rows(),
"slice window out of bounds for one or more tables"
);
}
let tables: Vec<Arc<Table>> = self
.tables
.iter()
.map(|t| Arc::new(t.slice_clone(offset, len)))
.collect();
let mut resolver = HashMap::new();
for (i, t) in tables.iter().enumerate() {
resolver.insert(t.name.clone(), i);
}
let name = format!("{}[{}, {})", self.name, offset, offset + len);
Cube {
tables,
name,
third_dim_index: self.third_dim_index.clone(),
resolver,
}
}
#[cfg(feature = "views")]
pub fn slice(&self, offset: usize, len: usize) -> CubeV {
assert!(!self.tables.is_empty(), "No tables to slice");
for t in &self.tables {
assert!(
offset + len <= t.n_rows(),
"slice window out of bounds for one or more tables"
);
}
self.tables
.iter()
.map(|t| TableV::from_table((**t).clone(), offset, len))
.collect()
}
#[cfg(feature = "parallel_proc")]
#[inline]
pub fn par_iter_tables(&self) -> rayon::slice::Iter<'_, Arc<Table>> {
self.tables.par_iter()
}
#[cfg(feature = "parallel_proc")]
#[inline]
pub fn par_iter_tables_mut(&mut self) -> rayon::slice::IterMut<'_, Arc<Table>> {
self.tables.par_iter_mut()
}
}
impl<'a> IntoIterator for &'a Cube {
type Item = &'a Arc<Table>;
type IntoIter = std::slice::Iter<'a, Arc<Table>>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.tables.iter()
}
}
impl<'a> IntoIterator for &'a mut Cube {
type Item = &'a mut Arc<Table>;
type IntoIter = std::slice::IterMut<'a, Arc<Table>>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.tables.iter_mut()
}
}
impl IntoIterator for Cube {
type Item = Arc<Table>;
type IntoIter = <Vec<Arc<Table>> as IntoIterator>::IntoIter;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.tables.into_iter()
}
}
impl Shape for Cube {
fn shape(&self) -> ShapeDim {
ShapeDim::Collection(self.tables.iter().map(|t| t.shape()).collect())
}
}
impl Concatenate for Cube {
fn concat(self, other: Self) -> Result<Self, MinarrowError> {
if self.tables.is_empty() && other.tables.is_empty() {
return Ok(Cube::new(
format!("{}+{}", self.name, other.name),
None,
None,
));
}
if self.tables.is_empty() {
let mut result = other;
result.name = format!("{}+{}", self.name, result.name);
return Ok(result);
}
if other.tables.is_empty() {
let mut result = self;
result.name = format!("{}+{}", result.name, other.name);
return Ok(result);
}
let self_schema: HashMap<String, ArrowType> = self.tables[0]
.cols()
.iter()
.map(|col| (col.field.name.clone(), col.field.dtype.clone()))
.collect();
let other_schema: HashMap<String, ArrowType> = other.tables[0]
.cols()
.iter()
.map(|col| (col.field.name.clone(), col.field.dtype.clone()))
.collect();
if self_schema.len() != other_schema.len() {
return Err(MinarrowError::IncompatibleTypeError {
from: "Cube",
to: "Cube",
message: Some(format!(
"Cannot concatenate cubes with different column counts: {} vs {}",
self_schema.len(),
other_schema.len()
)),
});
}
for (col_name, col_type) in &self_schema {
match other_schema.get(col_name) {
Some(other_type) if other_type == col_type => {}
Some(other_type) => {
return Err(MinarrowError::IncompatibleTypeError {
from: "Cube",
to: "Cube",
message: Some(format!(
"Column '{}' type mismatch: {:?} vs {:?}",
col_name, col_type, other_type
)),
});
}
None => {
return Err(MinarrowError::IncompatibleTypeError {
from: "Cube",
to: "Cube",
message: Some(format!(
"Column '{}' present in first cube but not in second",
col_name
)),
});
}
}
}
let mut result_tables = self.tables;
result_tables.extend(other.tables);
let mut resolver = HashMap::new();
for (i, t) in result_tables.iter().enumerate() {
resolver.insert(t.name.clone(), i);
}
Ok(Cube {
tables: result_tables,
name: format!("{}+{}", self.name, other.name),
third_dim_index: self.third_dim_index.clone(),
resolver,
})
}
}
#[cfg(all(feature = "views", feature = "select"))]
impl crate::traits::selection::ColumnSelection for Cube {
type View = Cube;
type ColumnView = Vec<crate::ArrayV>;
type ColumnOwned = Arc<Table>;
fn c<S: crate::traits::selection::FieldSelector>(&self, selection: S) -> Cube {
if self.tables.is_empty() {
return self.clone();
}
let fields = self.schema();
let mut indices = selection.resolve_fields(&fields);
if let Some(ref keys) = self.third_dim_index {
for key_name in keys {
if let Some(idx) = self.col_name_index(key_name) {
if !indices.contains(&idx) {
indices.push(idx);
}
}
}
}
indices.sort();
indices.dedup();
if indices.len() == fields.len() {
return self.clone();
}
let tables: Vec<Arc<Table>> = self.tables.iter().map(|t| {
let cols: Vec<FieldArray> = indices.iter()
.filter_map(|&i| t.cols().get(i).cloned())
.collect();
Arc::new(Table::new(t.name.clone(), Some(cols)))
}).collect();
let mut resolver = HashMap::new();
for (i, t) in tables.iter().enumerate() {
resolver.insert(t.name.clone(), i);
}
Cube {
tables,
name: self.name.clone(),
third_dim_index: self.third_dim_index.clone(),
resolver,
}
}
fn get(&self, field: &str) -> Option<Arc<Table>> {
let idx = self.resolver.get(field)?;
self.tables.get(*idx).cloned()
}
fn col_ix(&self, idx: usize) -> Option<Self::ColumnView> {
if self.tables.is_empty() || idx >= self.n_cols() {
return None;
}
Some(self.tables.iter().filter_map(|t| {
let fa = t.cols().get(idx)?;
Some(crate::ArrayV::new(fa.array.clone(), 0, fa.len()))
}).collect())
}
fn col_vec(&self) -> Vec<Self::ColumnView> {
if self.tables.is_empty() {
return Vec::new();
}
(0..self.n_cols()).filter_map(|i| {
crate::traits::selection::ColumnSelection::col_ix(self, i)
}).collect()
}
fn get_cols(&self) -> Vec<Arc<Field>> {
if self.tables.is_empty() {
Vec::new()
} else {
self.schema()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::structs::field_array::field_array;
use crate::traits::masked_array::MaskedArray;
use crate::traits::selection::ColumnSelection;
use crate::{Array, BooleanArray, IntegerArray, NumericArray};
fn build_test_table(name: &str, vals: &[i32], bools: &[bool]) -> Table {
let mut int_arr = IntegerArray::<i32>::default();
for v in vals {
int_arr.push(*v);
}
let mut bool_arr = BooleanArray::default();
for b in bools {
bool_arr.push(*b);
}
let mut t = Table::new(name.to_string(), None);
t.add_col(field_array("ints", Array::from_int32(int_arr)));
t.add_col(field_array("bools", Array::from_bool(bool_arr)));
t
}
#[test]
fn test_new_cube_empty() {
let c = Cube::new_empty();
assert_eq!(c.n_tables(), 0);
assert_eq!(c.n_rows().len(), 0);
assert!(c.is_empty());
assert!(c.tables().is_empty());
}
#[test]
fn test_add_and_get_tables_and_columns() {
let mut c = Cube::new_empty();
let t1 = build_test_table("t1", &[1, 2], &[true, false]);
let t2 = build_test_table("t2", &[3, 4], &[false, true]);
c.add_table(t1.clone());
c.add_table(t2.clone());
assert_eq!(c.n_tables(), 2);
assert_eq!(c.n_rows(), vec![2, 2]);
assert!(!c.is_empty());
assert_eq!(c.table_names(), vec!["t1", "t2"]);
assert!(c.has_table("t1"));
assert!(!c.has_table("notthere"));
assert_eq!(c.table_index("t2"), Some(1));
assert_eq!(c.table(0).unwrap().name, "t1");
assert_eq!(c.table(1).unwrap().name, "t2");
assert_eq!(c.n_cols(), 2);
assert_eq!(c.col_names(), vec!["ints", "bools"]);
assert!(c.has_col("ints"));
assert!(!c.has_col("nonexistent"));
let cols_by_idx = c.col_ix(0).unwrap();
assert_eq!(cols_by_idx.len(), 2);
assert_eq!(cols_by_idx[0].field.name, "ints");
assert_eq!(cols_by_idx[1].field.name, "ints");
let cols_by_name = c.col_by_name("bools").unwrap();
assert_eq!(cols_by_name.len(), 2);
assert_eq!(cols_by_name[0].field.name, "bools");
let mut seen: Vec<i32> = Vec::new();
for col in c.iter_cols_by_name("ints").unwrap() {
match &col.array {
Array::NumericArray(NumericArray::Int32(arr)) => seen.push(arr.get(0).unwrap()),
_ => panic!("Type mismatch"),
}
}
assert_eq!(seen, vec![1, 3]);
}
#[test]
#[should_panic]
fn test_add_table_schema_mismatch_panics() {
let mut c = Cube::new_empty();
let mut t1 = Table::new("t1".into(), None);
let mut arr = IntegerArray::<i32>::default();
arr.push(1);
t1.add_col(field_array("ints", Array::from_int32(arr)));
c.add_table(t1);
let mut t2 = Table::new("t2".into(), None);
let mut arr2 = IntegerArray::<i32>::default();
arr2.push(2);
t2.add_col(field_array("other", Array::from_int32(arr2))); c.add_table(t2); }
#[test]
fn test_remove_table_by_index_and_name() {
let mut c = Cube::new_empty();
let t1 = build_test_table("t1", &[1, 2], &[true, false]);
let t2 = build_test_table("t2", &[3, 4], &[false, true]);
c.add_table(t1.clone());
c.add_table(t2.clone());
assert!(c.remove_table("t1"));
assert_eq!(c.n_tables(), 1);
assert!(!c.has_table("t1"));
assert!(c.remove_table_at(0));
assert_eq!(c.n_tables(), 0);
assert!(!c.remove_table("not_there"));
assert!(!c.remove_table_at(5));
}
#[test]
fn test_remove_and_clear_column_across_tables() {
let mut c = Cube::new_empty();
let t1 = build_test_table("t1", &[1, 2], &[true, false]);
let t2 = build_test_table("t2", &[3, 4], &[false, true]);
c.add_table(t1.clone());
c.add_table(t2.clone());
assert!(c.remove_col("ints"));
assert!(!c.has_col("ints"));
assert_eq!(c.n_cols(), 1);
assert!(c.remove_col_at(0));
assert_eq!(c.n_cols(), 0);
assert!(!c.remove_col("doesnotexist"));
assert!(!c.remove_col_at(10));
}
#[test]
fn test_clear_cube() {
let mut c = Cube::new_empty();
let t = build_test_table("t1", &[1, 2, 3], &[true, false, true]);
c.add_table(t);
assert!(!c.is_empty());
c.clear();
assert!(c.is_empty());
assert_eq!(c.n_tables(), 0);
assert_eq!(c.n_rows().len(), 0);
}
#[test]
fn test_iter_tables_and_into_iter() {
let mut c = Cube::new_empty();
let t1 = build_test_table("t1", &[1], &[true]);
let t2 = build_test_table("t2", &[2], &[false]);
c.add_table(t1.clone());
c.add_table(t2.clone());
let names: Vec<_> = c.iter_tables().map(|t| t.name.as_str()).collect();
assert_eq!(names, ["t1", "t2"]);
let names2: Vec<_> = (&c).into_iter().map(|t| t.name.as_str()).collect();
assert_eq!(names2, ["t1", "t2"]);
}
#[test]
fn test_col_vec_method_and_schema() {
let mut c = Cube::new_empty();
let t1 = build_test_table("t1", &[1, 2], &[true, false]);
let t2 = build_test_table("t2", &[3, 4], &[false, true]);
c.add_table(t1);
c.add_table(t2);
let cols = c.col_vec();
assert_eq!(cols.len(), 2); assert_eq!(cols[0].len(), 2);
let schema = c.schema();
assert_eq!(schema[0].name, "ints");
assert_eq!(schema[1].name, "bools");
}
#[test]
fn test_iter_cols_and_col_variants() {
let mut c = Cube::new_empty();
let t1 = build_test_table("t1", &[1, 2], &[true, false]);
let t2 = build_test_table("t2", &[3, 4], &[false, true]);
c.add_table(t1.clone());
c.add_table(t2.clone());
let ints: Vec<i32> = c
.iter_cols(0)
.unwrap()
.map(|col| match &col.array {
Array::NumericArray(NumericArray::Int32(arr)) => arr.get(1).unwrap(),
_ => panic!("Type mismatch"),
})
.collect();
assert_eq!(ints, vec![2, 4]);
let bools: Vec<bool> = c
.iter_cols_by_name("bools")
.unwrap()
.map(|col| match &col.array {
Array::BooleanArray(arr) => arr.get(0).unwrap(),
_ => panic!("Type mismatch"),
})
.collect();
assert_eq!(bools, vec![true, false]);
}
#[test]
fn test_cube_new_named() {
let mut int_arr = IntegerArray::<i32>::default();
int_arr.push(42);
let mut bool_arr = BooleanArray::default();
bool_arr.push(true);
let cols = vec![
field_array("x", Array::from_int32(int_arr)),
field_array("flag", Array::from_bool(bool_arr)),
];
let table = Table::new("single".to_string(), Some(cols.clone()));
let cube = Cube {
tables: vec![Arc::new(table)],
name: "test".to_string(),
third_dim_index: Some(vec!["timestamp".to_string()]),
resolver: HashMap::from([("single".to_string(), 0)]),
};
assert_eq!(cube.n_tables(), 1);
assert_eq!(cube.n_cols(), 2);
assert_eq!(cube.name, "test");
assert_eq!(cube.third_dim_index().unwrap(), &["timestamp"]);
}
#[cfg(feature = "views")]
#[test]
fn test_cube_slice_and_slice_clone() {
use crate::structs::field_array::field_array;
use crate::{Array, BooleanArray, IntegerArray};
let mut col1 = IntegerArray::<i32>::default();
col1.push(1);
col1.push(2);
col1.push(3);
let mut col2 = BooleanArray::default();
col2.push(true);
col2.push(false);
col2.push(true);
let mut t1 = Table::new("snap1".into(), None);
t1.add_col(field_array("ints", Array::from_int32(col1)));
t1.add_col(field_array("bools", Array::from_bool(col2)));
let mut t2 = Table::new("snap2".into(), None);
let mut col3 = IntegerArray::<i32>::default();
col3.push(11);
col3.push(12);
col3.push(13);
let mut col4 = BooleanArray::default();
col4.push(false);
col4.push(true);
col4.push(false);
t2.add_col(field_array("ints", Array::from_int32(col3)));
t2.add_col(field_array("bools", Array::from_bool(col4)));
let mut cube = Cube::new_empty();
cube.add_table(t1);
cube.add_table(t2);
let view = cube.slice(1, 2); assert_eq!(view.len(), 2); assert_eq!(view[0].n_rows(), 2); assert_eq!(view[1].n_rows(), 2);
assert_eq!(view[0].col("bools").col_ix(0).unwrap().len(), 2); assert_eq!(view[1].col("bools").col_ix(0).unwrap().len(), 2); }
#[cfg(feature = "parallel_proc")]
#[test]
fn test_cube_par_iter_tables() {
use rayon::prelude::*;
let mut cube = Cube::new_empty();
let t1 = build_test_table("a", &[1, 2], &[true, false]);
let t2 = build_test_table("b", &[3, 4], &[false, true]);
cube.add_table(t1);
cube.add_table(t2);
let mut names: Vec<&str> = cube.par_iter_tables().map(|t| t.name.as_str()).collect();
names.sort_unstable();
assert_eq!(names, vec!["a", "b"]);
}
}