day14
This commit is contained in:
parent
e27c10d3b0
commit
e16cafaa52
3 changed files with 447 additions and 0 deletions
|
@ -4,6 +4,10 @@ version = "0.1.0"
|
||||||
authors = ["Stefan Schwarz <stefan@f2o.io>"]
|
authors = ["Stefan Schwarz <stefan@f2o.io>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "day1"
|
name = "day1"
|
||||||
path = "src/day1/main.rs"
|
path = "src/day1/main.rs"
|
||||||
|
@ -43,3 +47,7 @@ path = "src/day11/main.rs"
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "day12"
|
name = "day12"
|
||||||
path = "src/day12/main.rs"
|
path = "src/day12/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "day14"
|
||||||
|
path = "src/day14/main.rs"
|
||||||
|
|
56
input14
Normal file
56
input14
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
18 FHWM => 1 XGQNZ
|
||||||
|
4 FVPWN, 9 CQGW => 7 QGHT
|
||||||
|
22 KZMS, 1 DMCJL => 8 TWGCK
|
||||||
|
1 LMGQN, 1 DSWDM, 1 GKGZ => 1 TGPH
|
||||||
|
22 WCSW => 1 LVTG
|
||||||
|
13 JSWR => 4 GKGZ
|
||||||
|
162 ORE => 3 FVPWN
|
||||||
|
59 CQGW, 15 MSNG, 6 XGKRF, 10 LJRQ, 1 HRKGV, 15 RKVC => 1 FUEL
|
||||||
|
5 DMCJL => 1 QBLH
|
||||||
|
2 XDRJ, 2 RKVC => 8 CTCNL
|
||||||
|
1 QXHX => 5 GFPSK
|
||||||
|
22 QGHT, 6 GFPSK, 5 DHTPL => 3 CSDR
|
||||||
|
4 QGHT, 2 HFXD => 4 XDRJ
|
||||||
|
10 WQCGV, 1 JSWR, 21 RHTLN => 7 VTPC
|
||||||
|
11 CQGW, 1 FVPWN => 3 HFXD
|
||||||
|
5 VTPC => 2 NCXW
|
||||||
|
8 LDZVS => 6 DQLH
|
||||||
|
117 ORE => 2 KWZNB
|
||||||
|
3 TGPH, 1 JPFQ, 2 WHWLK, 5 RKVC, 16 DQLH => 9 LJRQ
|
||||||
|
14 KWZNB, 2 CQGW => 8 MLPK
|
||||||
|
6 LDZVS => 2 JSWR
|
||||||
|
1 RKVC, 8 HCGT, 9 DHTPL => 6 FHWM
|
||||||
|
3 DHTPL, 1 HWSR, 36 LDZVS => 6 DSWDM
|
||||||
|
5 WHWLK, 1 LJHWT, 8 HSTHS => 7 VMPX
|
||||||
|
22 ZJCDZ, 3 WQCGV => 5 DHTPL
|
||||||
|
10 LJHWT, 32 GFPSK, 2 RHTLN => 4 HFRMP
|
||||||
|
2 FKVD, 3 TWGCK, 1 HWSR => 1 RNLZW
|
||||||
|
2 CSDR, 3 DQLH, 2 HSTHS => 9 JPFQ
|
||||||
|
1 JSWR, 1 PCWS, 1 HFRMP => 3 XGKRF
|
||||||
|
2 QGHT, 9 LVTG, 3 QBLH => 7 RHTLN
|
||||||
|
10 LJHWT, 4 CTCNL => 8 QXHX
|
||||||
|
16 MLPK, 1 HFXD => 9 ZJCDZ
|
||||||
|
6 QGHT => 9 WCSW
|
||||||
|
4 HWSR, 4 MLPK, 1 KZMS => 3 BGZHQ
|
||||||
|
12 MLPK => 8 RKVC
|
||||||
|
1 HWSR, 1 VNWFS => 7 BGFJ
|
||||||
|
7 FHWM, 11 CTDF, 1 LDZVS => 2 VNWFS
|
||||||
|
4 CTDF => 4 HSTHS
|
||||||
|
2 ZJCDZ => 6 LJHWT
|
||||||
|
1 VMPX, 1 NCXW, 1 HSTHS, 41 XGKRF, 30 HLNG, 1 GKGZ => 7 HRKGV
|
||||||
|
1 XGQNZ, 10 PCWS, 3 BGFJ => 8 FKVD
|
||||||
|
1 GFPSK, 1 DMCJL, 1 LVTG => 5 XDTZB
|
||||||
|
3 WCSW => 5 KZMS
|
||||||
|
6 TWGCK, 1 QXHX, 4 BGFJ => 2 LMGQN
|
||||||
|
1 WCSW => 7 LDZVS
|
||||||
|
1 XDTZB, 9 VNWFS => 3 WHWLK
|
||||||
|
3 HFXD, 4 WCSW, 1 MLPK => 5 WQCGV
|
||||||
|
2 BGFJ, 1 HSTHS, 22 MDCB, 10 HWSR, 6 RNLZW, 8 GKGZ => 5 MSNG
|
||||||
|
4 QGHT, 1 FKVD => 7 MDCB
|
||||||
|
9 MLPK, 3 LJHWT => 7 DMCJL
|
||||||
|
121 ORE => 2 CQGW
|
||||||
|
9 DHTPL, 2 BGZHQ => 8 CTDF
|
||||||
|
2 JSWR, 30 RHTLN => 7 HLNG
|
||||||
|
2 QBLH => 7 PCWS
|
||||||
|
14 LVTG => 8 HWSR
|
||||||
|
7 DMCJL => 1 HCGT
|
383
src/day14/main.rs
Normal file
383
src/day14/main.rs
Normal file
|
@ -0,0 +1,383 @@
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::convert::*;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufRead, BufReader};
|
||||||
|
|
||||||
|
type Resource = String;
|
||||||
|
type Pipeline = HashMap<Resource, ResourceBuilder>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ResourceBuilder {
|
||||||
|
requirements: Vec<Resource>,
|
||||||
|
out_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(Vec<Resource>, usize)> for ResourceBuilder {
|
||||||
|
fn from(build: (Vec<Resource>, usize)) -> Self {
|
||||||
|
ResourceBuilder {
|
||||||
|
requirements: build.0,
|
||||||
|
out_count: build.1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct NanoFactory {
|
||||||
|
pipeline: Pipeline,
|
||||||
|
ordered: Vec<Resource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum NanoFactoryBuildState {
|
||||||
|
InputCount,
|
||||||
|
InputResource,
|
||||||
|
OutputArrow,
|
||||||
|
OutputCount,
|
||||||
|
OutputResource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NanoFactory {
|
||||||
|
/// sort resources
|
||||||
|
fn resolve_order(&mut self) {
|
||||||
|
self.ordered = vec![Resource::from("ORE")];
|
||||||
|
let mut sortable: HashMap<Resource, Vec<Resource>> = self
|
||||||
|
.pipeline
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.clone(), v.requirements.clone()))
|
||||||
|
.collect();
|
||||||
|
while sortable.len() > 0 {
|
||||||
|
sortable = sortable
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(k, v)| {
|
||||||
|
if v.iter().all(|i| self.ordered.contains(i)) {
|
||||||
|
self.ordered.push(k);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((k, v))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
self.ordered.reverse();
|
||||||
|
self.ordered.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// calculate how much ores are required for required_fuel
|
||||||
|
fn resource_usage(&self, required_fuel: u128) -> usize {
|
||||||
|
// create storage to track leftover resources
|
||||||
|
let mut storage: HashMap<Resource, u128> = HashMap::new();
|
||||||
|
storage.insert(Resource::from("FUEL"), required_fuel);
|
||||||
|
|
||||||
|
// calculate resource costs
|
||||||
|
self.ordered.iter().for_each(|res| {
|
||||||
|
let rb = self.pipeline.get(res).unwrap();
|
||||||
|
let count = (storage.get(res).unwrap().clone() + (rb.out_count as u128) - 1)
|
||||||
|
/ (rb.out_count as u128);
|
||||||
|
rb.requirements.iter().for_each(|req| {
|
||||||
|
storage
|
||||||
|
.entry(req.clone())
|
||||||
|
.and_modify(|v| *v += count)
|
||||||
|
.or_insert(count);
|
||||||
|
storage.insert(res.clone(), 0);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// return cost for ore
|
||||||
|
*storage.get("ORE").unwrap() as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generate a single fuel
|
||||||
|
fn generate_fuel(&self) -> usize {
|
||||||
|
let mut storage: HashMap<Resource, usize> = HashMap::new();
|
||||||
|
self.lookup_resource(&mut storage, "FUEL")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// recursively calculate the cost of a resource in ores
|
||||||
|
fn lookup_resource(&self, storage: &mut HashMap<Resource, usize>, resource: &str) -> usize {
|
||||||
|
if resource == "ORE" {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// use from storage
|
||||||
|
if let Some(count) = storage.get_mut(resource) {
|
||||||
|
if *count > 0 {
|
||||||
|
*count -= 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize storage with zero
|
||||||
|
if storage.get(resource).is_none() {
|
||||||
|
storage.insert(resource.into(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// build resource and and to storage
|
||||||
|
let required = self
|
||||||
|
.pipeline
|
||||||
|
.get(resource)
|
||||||
|
.expect("unable to find resource");
|
||||||
|
let cost = required.requirements.iter().fold(0, |mut acc, c| {
|
||||||
|
acc += self.lookup_resource(storage, c);
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
|
||||||
|
// update storage
|
||||||
|
let stored = storage.get_mut(resource).unwrap();
|
||||||
|
*stored += required.out_count - 1;
|
||||||
|
|
||||||
|
cost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for NanoFactory
|
||||||
|
where
|
||||||
|
T: BufRead,
|
||||||
|
{
|
||||||
|
fn from(bufreader: T) -> Self {
|
||||||
|
let mut f = NanoFactory::default();
|
||||||
|
let mut state = NanoFactoryBuildState::InputCount;
|
||||||
|
bufreader.split(b'\n').enumerate().for_each(|(nr, line)| {
|
||||||
|
let line = line.unwrap();
|
||||||
|
let mut line = line.into_iter().peekable();
|
||||||
|
let mut count_var = 0;
|
||||||
|
let mut cur_requirements = Vec::new();
|
||||||
|
loop {
|
||||||
|
match state {
|
||||||
|
// numeric input
|
||||||
|
NanoFactoryBuildState::InputCount | NanoFactoryBuildState::OutputCount => {
|
||||||
|
while let Some(c) = next_if_in_range(&mut line, 48..=57) {
|
||||||
|
count_var = count_var * 10 + (c as usize) - 48;
|
||||||
|
}
|
||||||
|
assert_eq!(line.next().unwrap(), b' ');
|
||||||
|
state = match state {
|
||||||
|
NanoFactoryBuildState::InputCount => {
|
||||||
|
NanoFactoryBuildState::InputResource
|
||||||
|
}
|
||||||
|
NanoFactoryBuildState::OutputCount => {
|
||||||
|
NanoFactoryBuildState::OutputResource
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// input resouce name
|
||||||
|
NanoFactoryBuildState::InputResource => {
|
||||||
|
let mut r = Resource::new();
|
||||||
|
while let Some(c) = next_if_in_range(&mut line, 65..=90) {
|
||||||
|
r.push(c.into());
|
||||||
|
}
|
||||||
|
for _ in 0..count_var {
|
||||||
|
cur_requirements.push(r.clone());
|
||||||
|
}
|
||||||
|
r.truncate(0);
|
||||||
|
count_var = 0;
|
||||||
|
let next = line.next();
|
||||||
|
match next {
|
||||||
|
Some(b',') => {
|
||||||
|
assert_eq!(line.next().unwrap(), b' ');
|
||||||
|
state = NanoFactoryBuildState::InputCount;
|
||||||
|
}
|
||||||
|
Some(b' ') => state = NanoFactoryBuildState::OutputArrow,
|
||||||
|
_ => panic!(
|
||||||
|
"invalid format in line {}, expected ' ' or ',', got '{:?}'",
|
||||||
|
nr, next
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// output resource name
|
||||||
|
NanoFactoryBuildState::OutputResource => {
|
||||||
|
let mut r = Resource::new();
|
||||||
|
while let Some(c) = next_if_in_range(&mut line, 65..=90) {
|
||||||
|
r.push(c.into());
|
||||||
|
}
|
||||||
|
f.pipeline
|
||||||
|
.insert(r, ResourceBuilder::from((cur_requirements, count_var)));
|
||||||
|
state = NanoFactoryBuildState::InputCount;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// arrow between input and output ' => '
|
||||||
|
NanoFactoryBuildState::OutputArrow => {
|
||||||
|
assert_eq!(line.next().unwrap(), b'=');
|
||||||
|
assert_eq!(line.next().unwrap(), b'>');
|
||||||
|
assert_eq!(line.next().unwrap(), b' ');
|
||||||
|
state = NanoFactoryBuildState::OutputCount
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_if_in_range<I, T>(
|
||||||
|
peekable: &mut std::iter::Peekable<I>,
|
||||||
|
range: std::ops::RangeInclusive<T>,
|
||||||
|
) -> Option<T>
|
||||||
|
where
|
||||||
|
I: std::iter::Iterator<Item = T>,
|
||||||
|
T: Copy + std::cmp::PartialOrd,
|
||||||
|
{
|
||||||
|
if let Some(peeked) = peekable.peek() {
|
||||||
|
if range.contains(peeked) {
|
||||||
|
peekable.next()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let f = File::open("input14").unwrap();
|
||||||
|
let bufreader = BufReader::new(f);
|
||||||
|
let mut factory = NanoFactory::from(bufreader);
|
||||||
|
|
||||||
|
// part1
|
||||||
|
println!("ores: {}", factory.generate_fuel());
|
||||||
|
|
||||||
|
// part2 as binary search
|
||||||
|
factory.resolve_order();
|
||||||
|
let mut min = 1;
|
||||||
|
let mut max = 1_000_000_000_000;
|
||||||
|
let mut fuel;
|
||||||
|
loop {
|
||||||
|
fuel = (min + max) / 2;
|
||||||
|
let ore = factory.resource_usage(fuel);
|
||||||
|
match 1_000_000_000_000.cmp(&ore) {
|
||||||
|
Ordering::Less => max = fuel - 1,
|
||||||
|
Ordering::Greater => {
|
||||||
|
min = fuel + 1;
|
||||||
|
if min > max {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ordering::Equal => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
println!("ammount: {}", fuel);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn example1() {
|
||||||
|
let bufreader = BufReader::new(
|
||||||
|
"10 ORE => 10 A
|
||||||
|
1 ORE => 1 B
|
||||||
|
7 A, 1 B => 1 C
|
||||||
|
7 A, 1 C => 1 D
|
||||||
|
7 A, 1 D => 1 E
|
||||||
|
7 A, 1 E => 1 FUEL
|
||||||
|
"
|
||||||
|
.trim()
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
let factory = NanoFactory::from(bufreader);
|
||||||
|
assert_eq!(factory.generate_fuel(), 31);
|
||||||
|
assert_eq!(factory.resource_usage(1), 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn example2() {
|
||||||
|
let bufreader = BufReader::new(
|
||||||
|
"
|
||||||
|
9 ORE => 2 A
|
||||||
|
8 ORE => 3 B
|
||||||
|
7 ORE => 5 C
|
||||||
|
3 A, 4 B => 1 AB
|
||||||
|
5 B, 7 C => 1 BC
|
||||||
|
4 C, 1 A => 1 CA
|
||||||
|
2 AB, 3 BC, 4 CA => 1 FUEL
|
||||||
|
"
|
||||||
|
.trim()
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
let factory = NanoFactory::from(bufreader);
|
||||||
|
assert_eq!(factory.generate_fuel(), 165);
|
||||||
|
assert_eq!(factory.resource_usage(1), 165);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn example3() {
|
||||||
|
let bufreader = BufReader::new(
|
||||||
|
"
|
||||||
|
157 ORE => 5 NZVS
|
||||||
|
165 ORE => 6 DCFZ
|
||||||
|
44 XJWVT, 5 KHKGT, 1 QDVJ, 29 NZVS, 9 GPVTF, 48 HKGWZ => 1 FUEL
|
||||||
|
12 HKGWZ, 1 GPVTF, 8 PSHF => 9 QDVJ
|
||||||
|
179 ORE => 7 PSHF
|
||||||
|
177 ORE => 5 HKGWZ
|
||||||
|
7 DCFZ, 7 PSHF => 2 XJWVT
|
||||||
|
165 ORE => 2 GPVTF
|
||||||
|
3 DCFZ, 7 NZVS, 5 HKGWZ, 10 PSHF => 8 KHKGT
|
||||||
|
"
|
||||||
|
.trim()
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
let factory = NanoFactory::from(bufreader);
|
||||||
|
assert_eq!(factory.generate_fuel(), 13312);
|
||||||
|
assert_eq!(factory.generate_fuel_from_ores(1_000_000_000_000), 82892753);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn example4() {
|
||||||
|
let bufreader = BufReader::new(
|
||||||
|
"
|
||||||
|
2 VPVL, 7 FWMGM, 2 CXFTF, 11 MNCFX => 1 STKFG
|
||||||
|
17 NVRVD, 3 JNWZP => 8 VPVL
|
||||||
|
53 STKFG, 6 MNCFX, 46 VJHF, 81 HVMC, 68 CXFTF, 25 GNMV => 1 FUEL
|
||||||
|
22 VJHF, 37 MNCFX => 5 FWMGM
|
||||||
|
139 ORE => 4 NVRVD
|
||||||
|
144 ORE => 7 JNWZP
|
||||||
|
5 MNCFX, 7 RFSQX, 2 FWMGM, 2 VPVL, 19 CXFTF => 3 HVMC
|
||||||
|
5 VJHF, 7 MNCFX, 9 VPVL, 37 CXFTF => 6 GNMV
|
||||||
|
145 ORE => 6 MNCFX
|
||||||
|
1 NVRVD => 8 CXFTF
|
||||||
|
1 VJHF, 6 MNCFX => 4 RFSQX
|
||||||
|
176 ORE => 6 VJHF
|
||||||
|
"
|
||||||
|
.trim()
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
let factory = NanoFactory::from(bufreader);
|
||||||
|
assert_eq!(factory.generate_fuel(), 180697);
|
||||||
|
assert_eq!(factory.resource_usage(1), 180697);
|
||||||
|
// TODO assert_eq!(factory.generate_fuel_from_ores(1_000_000_000_000), 5586022);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn example5() {
|
||||||
|
let bufreader = BufReader::new(
|
||||||
|
"
|
||||||
|
171 ORE => 8 CNZTR
|
||||||
|
7 ZLQW, 3 BMBT, 9 XCVML, 26 XMNCP, 1 WPTQ, 2 MZWV, 1 RJRHP => 4 PLWSL
|
||||||
|
114 ORE => 4 BHXH
|
||||||
|
14 VRPVC => 6 BMBT
|
||||||
|
6 BHXH, 18 KTJDG, 12 WPTQ, 7 PLWSL, 31 FHTLT, 37 ZDVW => 1 FUEL
|
||||||
|
6 WPTQ, 2 BMBT, 8 ZLQW, 18 KTJDG, 1 XMNCP, 6 MZWV, 1 RJRHP => 6 FHTLT
|
||||||
|
15 XDBXC, 2 LTCX, 1 VRPVC => 6 ZLQW
|
||||||
|
13 WPTQ, 10 LTCX, 3 RJRHP, 14 XMNCP, 2 MZWV, 1 ZLQW => 1 ZDVW
|
||||||
|
5 BMBT => 4 WPTQ
|
||||||
|
189 ORE => 9 KTJDG
|
||||||
|
1 MZWV, 17 XDBXC, 3 XCVML => 2 XMNCP
|
||||||
|
12 VRPVC, 27 CNZTR => 2 XDBXC
|
||||||
|
15 KTJDG, 12 BHXH => 5 XCVML
|
||||||
|
3 BHXH, 2 VRPVC => 7 MZWV
|
||||||
|
121 ORE => 7 VRPVC
|
||||||
|
7 XCVML => 6 RJRHP
|
||||||
|
5 BHXH, 4 VRPVC => 5 LTCX
|
||||||
|
"
|
||||||
|
.trim()
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
let factory = NanoFactory::from(bufreader);
|
||||||
|
assert_eq!(factory.generate_fuel(), 2210736);
|
||||||
|
assert_eq!(factory.resource_usage(1), 2210736);
|
||||||
|
// TODO assert_eq!(factory.generate_fuel_from_ores(1_000_000_000_000), 460664);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue