ASCII art diagram converter: Difference between revisions

Add source for Rust
No edit summary
(Add source for Rust)
Line 470:
packer=:1 :0
w=. -#;.1 ,partition normalize m
_8 (#.\ ;) w ({. #:)&.> ]
)
 
Line 499:
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 
Line 548:
public class AsciiArtDiagramConverter {
 
private static final String TEST = "+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+\r\n" +
"| ID |\r\n" +
"+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+\r\n" +
"|QR| Opcode |AA|TC|RD|RA| Z | RCODE |\r\n" +
"+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+\r\n" +
"| QDCOUNT |\r\n" +
"+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+\r\n" +
"| ANCOUNT |\r\n" +
"+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+\r\n" +
"| NSCOUNT |\r\n" +
"+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+\r\n" +
"| ARCOUNT |\r\n" +
"+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+";
 
public static void main(String[] args) {
validate(TEST);
Line 569:
displayCode(asciiMap, "78477bbf5496e12e1bf169a4");
}
 
private static void displayCode(Map<String,List<Integer>> asciiMap, String hex) {
System.out.printf("%nTest string in hex:%n%s%n%n", hex);
 
String bin = new BigInteger(hex,16).toString(2);
 
// Zero pad in front as needed
int length = 0;
Line 596:
 
}
 
 
private static void display(String ascii) {
System.out.printf("%nDiagram:%n%n");
Line 607:
private static void displayMap(Map<String,List<Integer>> asciiMap) {
System.out.printf("%nDecode:%n%n");
 
 
System.out.printf("Name Size Start End%n");
System.out.printf("-------- ----- ----- -----%n");
Line 615:
System.out.printf("%-8s %2d %2d %2d%n", code, pos.get(1)-pos.get(0)+1, pos.get(0), pos.get(1));
}
 
}
 
private static Map<String,List<Integer>> decode(String ascii) {
Map<String,List<Integer>> map = new LinkedHashMap<>();
String[] split = TEST.split("\\r\\n");
Line 638:
}
}
 
return map;
}
Line 676:
}
else {
// Vertical bar, followed by optional spaces, followed by name, followed by optional spaces, followed by vdrtical bar
if ( ! test.matches("^\\|(\\s*[A-Za-z]+\\s*\\|)+$") ) {
throw new RuntimeException("ERROR 6: Improper line format. Line = " + test);
Line 916:
ARCOUNT is 16 bits
 
template = A16 A1 A4 A1 A1 A1 A1 A3 A4 A16 A16 A16 A16
 
(
Line 970:
offset += l
if k=w then exit end if
end while
end if
end for
Line 995:
--
-- split's ",no_empty:=true)" is not quite enough here.
-- Note that if copy/paste slips in any tab characters,
-- it will most likely trigger a length mismatch error.
--
Line 1,032:
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 
"""
 
Line 1,121:
(define bit-re #px"[|+]([^|+]*)")
(define cell-re #px"[|]([^|]*)")
 
(define bit-boundaries (regexp-match-positions* bit-re (car lines)))
 
(define bits/word (sub1 (length bit-boundaries)))
 
(unless (zero? (modulo bits/word 8))
(error 'ascii-art->struct "diagram is not a multiple of 8 bits wide"))
 
(define-values (pos->bit-start# pos->bit-end#)
(for/fold ((s# (hash)) (e# (hash)))
Line 1,136:
(values (hash-set s# (car boundary) bit)
(hash-set e# (cdr boundary) bit))))
 
(define fields
(apply append
Line 1,168:
(word-size (add1 (car (for/last ((f all-fields)) f))))
(fld-ids (map cadddr all-fields))
 
(fld-setters
(cons
Line 1,182:
,(caddr fld)
,(add1 (cadr fld)))))))
 
(set-fields-bits
(list*
Line 1,212:
#`(begin
(struct id fld-ids #:mutable)
 
(define (bytes->id bs)
fld-setters)
 
(define (id->bytes art-in)
(match-define (id #,@#'fld-ids) art-in)
Line 1,429:
 
=={{header|REXX}}==
Some code was added to validate the input file.
<lang rexx>/*REXX program interprets an ASCII art diagram for names and their bit length(s). */
numeric digits 100 /*be able to handle large numbers. */
Line 1,538:
NSCOUNT decimal= 32896 hex= 8080
ARCOUNT decimal= 10 hex= 000a
</pre>
 
=={{header|Rust}}==
The solution implements a few additional features:
 
* The input is thoroughly validated.
* The width is intentionally not restricted, therefore diagrams of any width can be processed.
* The parser allows omitting border for fields that occupy multiple full lines.
* Fields can wrap over an end of a line.
* When extracting field values, truncated input is recognized and unfilled fields are reported.
 
 
See the output below the source code.
 
<lang Rust>use std::{borrow::Cow, io::Write};
 
pub type Bit = bool;
 
#[derive(Clone, Debug)]
pub struct Field {
name: String,
from: usize,
to: usize,
}
 
impl Field {
pub fn new(name: String, from: usize, to: usize) -> Self {
assert!(from < to);
Self { name, from, to }
}
 
pub fn name(&self) -> &str {
&self.name
}
 
pub fn from(&self) -> usize {
self.from
}
 
pub fn to(&self) -> usize {
self.to
}
 
pub fn size(&self) -> usize {
self.to - self.from
}
 
pub fn extract_bits<'a>(
&self,
bytes: &'a [u8],
) -> Option<impl Iterator<Item = (usize, Bit)> + 'a> {
if self.to <= bytes.len() * 8 {
Some((self.from..self.to).map(move |index| {
let byte = bytes[index / 8];
let bit_index = 7 - (index % 8);
let bit_value = (byte >> bit_index) & 1 == 1;
(index, bit_value)
}))
} else {
None
}
}
 
fn extend(&mut self, new_to: usize) {
assert!(self.to <= new_to);
self.to = new_to;
}
}
 
trait Consume: Iterator {
fn consume(&mut self, value: Self::Item) -> Result<Self::Item, Option<Self::Item>>
where
Self::Item: PartialEq,
{
match self.next() {
Some(v) if v == value => Ok(v),
Some(v) => Err(Some(v)),
None => Err(None),
}
}
}
 
impl<T: Iterator> Consume for T {}
 
#[derive(Clone, Copy, Debug)]
enum ParserState {
Uninitialized,
ExpectBorder,
ExpectField,
AllowEmpty,
}
 
#[derive(Clone, Copy, Debug)]
pub enum ParserError {
ParsingFailed,
UnexpectedEnd,
InvalidBorder,
WrongLineWidth,
FieldExpected,
BadField,
}
 
#[derive(Debug)]
pub(crate) struct Parser {
state: Option<ParserState>,
width: usize,
from: usize,
fields: Vec<Field>,
}
 
impl Parser {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
state: Some(ParserState::Uninitialized),
width: 0,
from: 0,
fields: Vec::new(),
}
}
 
pub fn accept(&mut self, line: &str) -> Result<(), ParserError> {
if let Some(state) = self.state.take() {
let line = line.trim();
 
if !line.is_empty() {
self.state = Some(match state {
ParserState::Uninitialized => self.parse_border(line)?,
ParserState::ExpectBorder => self.accept_border(line)?,
ParserState::ExpectField => self.parse_fields(line)?,
ParserState::AllowEmpty => self.extend_field(line)?,
});
}
 
Ok(())
} else {
Err(ParserError::ParsingFailed)
}
}
 
pub fn finish(self) -> Result<Vec<Field>, ParserError> {
match self.state {
Some(ParserState::ExpectField) => Ok(self.fields),
_ => Err(ParserError::UnexpectedEnd),
}
}
 
fn parse_border(&mut self, line: &str) -> Result<ParserState, ParserError> {
self.width = Parser::border_columns(line).map_err(|_| ParserError::InvalidBorder)?;
Ok(ParserState::ExpectField)
}
 
fn accept_border(&mut self, line: &str) -> Result<ParserState, ParserError> {
match Parser::border_columns(line) {
Ok(width) if width == self.width => Ok(ParserState::ExpectField),
Ok(_) => Err(ParserError::WrongLineWidth),
Err(_) => Err(ParserError::InvalidBorder),
}
}
 
fn parse_fields(&mut self, line: &str) -> Result<ParserState, ParserError> {
let mut slots = line.split('|');
// The first split result is the space outside of the schema
slots.consume("").map_err(|_| ParserError::FieldExpected)?;
let mut remaining_width = self.width * Parser::COLUMN_WIDTH;
let mut fields_found = 0;
 
loop {
match slots.next() {
Some(slot) if slot.is_empty() => {
// The only empty slot is the last one
if slots.next().is_some() || remaining_width != 0 {
return Err(ParserError::BadField);
}
 
break;
}
 
Some(slot) => {
let slot_width = slot.chars().count() + 1; // Include the slot separator
if remaining_width < slot_width || slot_width % Parser::COLUMN_WIDTH != 0 {
return Err(ParserError::BadField);
}
 
let name = slot.trim();
 
if name.is_empty() {
return Err(ParserError::BadField);
}
 
// An actual field slot confirmed
remaining_width -= slot_width;
fields_found += 1;
let from = self.from;
let to = from + slot_width / Parser::COLUMN_WIDTH;
// If the slot belongs to the same field as the last one, just extend it
if let Some(f) = self.fields.last_mut().filter(|f| f.name() == name) {
f.extend(to);
} else {
self.fields.push(Field::new(name.to_string(), from, to));
}
 
self.from = to;
}
 
_ => return Err(ParserError::BadField),
}
}
 
Ok(if fields_found == 1 {
ParserState::AllowEmpty
} else {
ParserState::ExpectBorder
})
}
 
fn extend_field(&mut self, line: &str) -> Result<ParserState, ParserError> {
let mut slots = line.split('|');
// The first split result is the space outside of the schema
if slots.consume("").is_ok() {
if let Some(slot) = slots.next() {
if slots.consume("").is_ok() {
let slot_width = slot.chars().count() + 1;
let remaining_width = self.width * Parser::COLUMN_WIDTH;
if slot_width == remaining_width && slot.chars().all(|c| c == ' ') {
self.from += self.width;
self.fields.last_mut().unwrap().extend(self.from);
return Ok(ParserState::AllowEmpty);
}
}
}
}
 
self.accept_border(line)
}
 
const COLUMN_WIDTH: usize = 3;
 
fn border_columns(line: &str) -> Result<usize, Option<char>> {
let mut chars = line.chars();
 
// Read the first cell, which is mandatory
chars.consume('+')?;
chars.consume('-')?;
chars.consume('-')?;
chars.consume('+')?;
let mut width = 1;
 
loop {
match chars.consume('-') {
Err(Some(c)) => return Err(Some(c)),
Err(None) => return Ok(width),
Ok(_) => {}
}
 
chars.consume('-')?;
chars.consume('+')?;
width += 1;
}
}
}
 
pub struct Fields(pub Vec<Field>);
 
#[derive(Clone, Debug)]
pub struct ParseFieldsError {
pub line: Option<String>,
pub kind: ParserError,
}
 
impl ParseFieldsError {
fn new(line: Option<String>, kind: ParserError) -> Self {
Self { line, kind }
}
}
 
impl std::str::FromStr for Fields {
type Err = ParseFieldsError;
 
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parser = Parser::new();
for line in s.lines() {
parser
.accept(line)
.map_err(|e| ParseFieldsError::new(Some(line.to_string()), e))?;
}
 
parser
.finish()
.map(Fields)
.map_err(|e| ParseFieldsError::new(None, e))
}
}
 
impl Fields {
pub fn print_schema(&self, f: &mut dyn Write) -> std::io::Result<()> {
writeln!(f, "Name Bits Start End")?;
writeln!(f, "=================================")?;
for field in self.0.iter() {
writeln!(
f,
"{:<12} {:>5} {:>3} {:>3}",
field.name(),
field.size(),
field.from(),
field.to() - 1 // Range is exclusive, but display it as inclusive
)?;
}
writeln!(f)
}
 
pub fn print_decode(&self, f: &mut dyn Write, bytes: &[u8]) -> std::io::Result<()> {
writeln!(f, "Input (hexadecimal octets): {:x?}", bytes)?;
writeln!(f)?;
writeln!(f, "Name Size Bit pattern")?;
writeln!(f, "=================================")?;
for field in self.0.iter() {
writeln!(
f,
"{:<12} {:>5} {}",
field.name(),
field.size(),
field
.extract_bits(&bytes)
.map(|it| it.fold(String::new(), |mut acc, (index, bit)| {
// Instead of simple collect, let's print it rather with
// byte boundaries visible as spaces
if index % 8 == 0 && !acc.is_empty() {
acc.push(' ');
}
acc.push(if bit { '1' } else { '0' });
acc
}))
.map(Cow::Owned)
.unwrap_or_else(|| Cow::Borrowed("N/A"))
)?;
}
 
writeln!(f)
}
}
 
fn normalize(diagram: &str) -> String {
diagram
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.fold(String::new(), |mut acc, x| {
if !acc.is_empty() {
acc.push('\n');
}
 
acc.push_str(x);
acc
})
}
 
fn main() {
let diagram = r"
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| OVERSIZED |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| OVERSIZED | unused |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
";
 
let data = b"\x78\x47\x7b\xbf\x54\x96\xe1\x2e\x1b\xf1\x69\xa4\xab\xcd\xef\xfe\xdc";
 
// Normalize and print the input, there is no need and no requirement to
// generate it from the parsed representation
let diagram = normalize(diagram);
println!("{}", diagram);
println!();
 
match diagram.parse::<Fields>() {
Ok(fields) => {
let mut stdout = std::io::stdout();
fields.print_schema(&mut stdout).ok();
fields.print_decode(&mut stdout, data).ok();
}
 
Err(ParseFieldsError {
line: Some(line),
kind: e,
}) => eprintln!("Invalid input: {:?}\n{}", e, line),
 
Err(ParseFieldsError {
line: _,
kind: e,
}) => eprintln!("Could not parse the input: {:?}", e),
}
}</lang>
 
{{out}}
<pre>
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| OVERSIZED |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| OVERSIZED | unused |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 
Name Bits Start End
=================================
ID 16 0 15
QR 1 16 16
Opcode 4 17 20
AA 1 21 21
TC 1 22 22
RD 1 23 23
RA 1 24 24
Z 3 25 27
RCODE 4 28 31
QDCOUNT 16 32 47
ANCOUNT 16 48 63
NSCOUNT 16 64 79
ARCOUNT 16 80 95
OVERSIZED 36 96 131
unused 12 132 143
 
Input (hexadecimal octets): [78, 47, 7b, bf, 54, 96, e1, 2e, 1b, f1, 69, a4, ab, cd, ef, fe, dc]
 
Name Size Bit pattern
=================================
ID 16 01111000 01000111
QR 1 0
Opcode 4 1111
AA 1 0
TC 1 1
RD 1 1
RA 1 1
Z 3 011
RCODE 4 1111
QDCOUNT 16 01010100 10010110
ANCOUNT 16 11100001 00101110
NSCOUNT 16 00011011 11110001
ARCOUNT 16 01101001 10100100
OVERSIZED 36 10101011 11001101 11101111 11111110 1101
unused 12 N/A
</pre>
 
Anonymous user