10. Standard library

data:: · Index · Next: CLI commands →

List and string methods, plus the global functions available anywhere in a script.


10.1 List methods

Iterating

list.each { |item| ... }

Runs the block for every item.

check "each" {
  doc.pages.each { |page|
    assert page.width > 0, "page #{page.number} has no width"
  }
}

list.each_with_index { |item, i| ... }

Like each, but the second parameter receives the position (starting at 0).

check "each_with_index" {
  doc.fonts.each_with_index { |font, i|
    print("font", i + 1, "of", doc.fonts.length, ":", font.name)
  }
}

Testing

list.all { |item| ... }

True when every item satisfies the condition. An empty list returns true.

check "all" {
  require doc.fonts.all { |f| f.is_embedded }
  require doc.pages.all { |p| p.has_trim_box }
}

list.any { |item| ... }

True when at least one item satisfies it. An empty list returns false.

check "any" {
  assert doc.pages.any { |p| p.extract_text() != "" },
    "the entire document has no text"
}

list.contains(value)

True when the value is in the list.

check "contains" {
  require [1, 2, 3].contains(2)
  require prepress::detect_spot_colors().contains("Varnish")
}

Transforming

list.filter { |item| ... }

A new list holding only the items that satisfy the condition.

check "filter" {
  bad = doc.images.filter { |img| img.dpi < 300 }
  assert bad.length == 0,
    "#{bad.length} image(s) with low resolution"

  // Chaining: filter and then transform
  names = doc.fonts.filter { |f| !f.is_embedded }.map { |f| f.name }
  print("loose fonts:", names.join(", "))
}

list.map { |item| ... }

A new list holding the result of the block for each item.

check "map" {
  numbers = doc.pages.map { |p| p.number }
  widths = doc.pages.map { |p| p.width }
  print("pages:", numbers.join(", "))
}

Accessing

list.length

Item count. Works as a property or a method: list.length and list.length() are equivalent.

check "length" {
  require doc.pages.length == doc.page_count
  print("fonts:", doc.fonts.length)
}

list.get(n)

The nth item, 1-based. Friendly error when the index does not exist.

check "get" {
  row = data::load_dataset("data/batches.csv").get(2)   // second row
  print("first column:", row.get(1))
}

list.first() and list.last()

First and last item. Both return null on an empty list (no error).

check "first and last" {
  first = doc.pages.first()
  last = doc.pages.last()
  print("from page", first.number, "to", last.number)

  // Safe on an empty list: null is falsy
  spots = prepress::detect_spot_colors()
  assert !spots.first() || spots.first() == "Varnish",
    "unexpected special ink: #{spots.first()}"
}

list.join([separator])

Joins items into text. Default separator: ", ".

check "join" {
  print(doc.fonts.map { |f| f.name }.join(", "))
  print(prepress::get_page_boxes(1).join(" | "))
  print([1, 2, 3].join(" -> "))
}

10.2 String methods

Method What it does
text.contains(sub) Does it contain the fragment?
text.starts_with(sub) Does it start with it?
text.ends_with(sub) Does it end with it?
text.trim() Strips whitespace from both ends
text.to_uppercase() All uppercase
text.to_lowercase() All lowercase
text.length Character count
check "String methods" {
  title = doc.title

  require title.length > 0
  require title.trim() == title          // no stray whitespace
  assert !title.to_lowercase().contains("draft"),
    "title still marked as draft"

  code = codes::decode_barcode(1)
  assert code.starts_with("789"), "GTIN is not Brazilian"

  file = doc.filename
  assert file.ends_with(".pdf"), "unexpected extension"
}

The difference between contains on a string and on a list:

check "contains on each type" {
  // string: looks for a FRAGMENT inside the text
  require "final document".contains("final")

  // list: looks for a whole ITEM
  require ["a", "b"].contains("a")
  require !["ab"].contains("a")      // "a" is not an item of the list
}

10.3 Global functions

min(a, b) and max(a, b)

check "min and max" {
  widths = doc.pages.map { |p| p.width }
  // Reducing a list with each
  smallest = 99999
  doc.pages.each { |p| smallest = min(smallest, p.width) }
  print("narrowest page:", smallest, "pt")
}

abs(x)

Absolute value — essential for comparing dimensions with a tolerance.

check "abs" {
  const A4_WIDTH = 595.0
  const TOLERANCE = 5.0

  doc.pages.each { |page|
    // "the difference, either way, is smaller than the tolerance"
    assert abs(page.width - A4_WIDTH) < TOLERANCE,
      "page #{page.number} is outside A4: #{page.width}pt"
  }
}

round(x)

Rounds to the nearest integer. Useful for keeping messages readable.

check "round" {
  doc.images.each { |img|
    // without round: "217.4453125 DPI" — with round: "217 DPI"
    assert img.dpi >= 300,
      "image on page #{img.page_number}: #{round(img.dpi)} DPI"
  }

  mb = struct::file_size() / 1024 / 1024
  print("size:", round(mb), "MB")
}

print(...)

Prints values separated by spaces. It goes to stderr, so it never pollutes the report on stdout — you can use > report.json without mixing the two.

check "print" {
  print("document:", doc.filename)
  print("pages:", doc.page_count, "| fonts:", doc.fonts.length)

  // Handy for exploring before writing the final validation
  doc.images.each { |img|
    print("image", img.width, "x", img.height, "@", round(img.dpi), "DPI")
  }
}

region(x, y, width, height [, name])

Creates a region. Documented in chapter 2.


10.4 Useful patterns

Counting how many items fail

check "Problem count" {
  bad = doc.images.filter { |i| i.dpi < 300 }
  assert bad.length == 0,
    "#{bad.length} of #{doc.images.length} images below 300 DPI"
}

Listing the failures in the message

check "List in the message" {
  // Chains stay on one line: the dot must follow the previous value
  // with no line break in between.
  problems = doc.pages.filter { |p| !p.has_trim_box }.map { |p| p.number }

  assert problems.length == 0,
    "pages without a TrimBox: #{problems.join(", ")}"
}

Validating with a tolerance

function close_to(value, target, tolerance) {
  abs(value - target) < tolerance
}

check "With tolerance" {
  doc.pages.each { |page|
    assert close_to(page.width, 595.0, 2.0),
      "page #{page.number}: width #{page.width}pt (expected 595 ± 2)"
  }
}

Avoiding errors on an empty document

check "Defensive" {
  // Short-circuiting avoids calling first() on an empty list
  assert doc.pages.length == 0 || doc.pages.first().width > 0,
    "the first page has no width"
}

data:: · Index · Next: CLI commands →