DEV Community

Vin Lookup
Vin Lookup

Posted on

Displaying EngineCylinders from vPIC Without Inventing Performance Claims

NHTSA vPIC often returns an EngineCylinders field on DecodeVinValues-style payloads: a count such as "4", "6", or "8" when the pattern includes it. That number is useful on a free VIN decode card. The trap is turning it into a performance pitch -- "V8 power," "sporty four-banger," "tows like a diesel six" -- that a cylinder count never asserted.

This post is about honest display: show EngineCylinders when vPIC provides it, refuse performance inventing, and allow a clean "not provided" state when the field is empty. Keep cylinder catalog values separate from EngineHP, displacement, ForcedInduction, and brochure 0-60 claims. (Horsepower honesty is a separate topic; here the focus is the cylinder field alone.)

What EngineCylinders is (and is not)

EngineCylinders is a catalog attribute associated with the VIN decode. It answers a narrow question: which cylinder count did the decode associate with this pattern?

It does not answer:

  • Horsepower, torque, or dyno results
  • Whether the layout is V, inline, flat, or rotary marketing language you prefer
  • Forced induction, tune status, or fuel system health
  • Towing capacity or 0-60 times
  • Whether the engine in the listed vehicle is original and unmodified

Empty EngineCylinders does not authorize a default "4" badge, and "8" does not mean "V8 muscle." Do not fill gaps from Make/Model/Year folklore or a chart keyed only by cylinder count.

Normalize empties, do not invent power

Treat blank, "Not Applicable", "N/A", and similar tokens as missing. Keep a sourced integer when valid; do not rewrite "6" into "V6 Twin-Turbo" or "high output."

const EMPTY = new Set([
  "",
  "not applicable",
  "n/a",
  "na",
  "null",
  "none",
  "unknown",
]);

export type EngineCylindersView = {
  cylinders: number | null;
  raw: string | null;
  source: "vpic" | "missing";
};

export function viewEngineCylinders(fields: {
  EngineCylinders?: string | null;
}): EngineCylindersView {
  const raw = (fields.EngineCylinders ?? "").trim();
  if (!raw || EMPTY.has(raw.toLowerCase())) {
    return { cylinders: null, raw: null, source: "missing" };
  }
  // Digits only become an int; other catalog noise stays raw-sourced.
  if (!/^\d+$/.test(raw)) {
    return { cylinders: null, raw, source: "vpic" };
  }
  const n = Number.parseInt(raw, 10);
  if (!Number.isFinite(n) || n <= 0) {
    return { cylinders: null, raw, source: "vpic" };
  }
  return { cylinders: n, raw, source: "vpic" };
}

export function engineCylindersLines(
  view: EngineCylindersView,
): string[] {
  if (view.source === "missing") {
    return [
      "Engine cylinders: not provided by vPIC for this VIN",
    ];
  }
  if (view.cylinders != null) {
    return [
      `Engine cylinders (vPIC): ${view.cylinders}`,
      "Catalog count only -- not a horsepower or tow rating claim",
    ];
  }
  return [
    `Engine cylinders (vPIC): ${view.raw}`,
    "Catalog value only -- not a horsepower or tow rating claim",
  ];
}
Enter fullscreen mode Exit fullscreen mode

The footnote matters. Buyers over-read "8" as a V8 package. Show the sourced count; if blank, say "not provided" -- no greyed "V6" and no invented HP from cylinders alone.

Forbidden upgrades

Product pressure often asks for:

  1. Mapping 4/6/8 into "efficient / balanced / powerful" marketing adjectives
  2. Prefixing "V" or "I" without a sourced engine configuration field
  3. Inferring tow ratings or 0-60 from cylinder count
  4. Defaulting blank EngineCylinders to "4" so economy cars look complete
  5. Merging cylinders with FuelTypePrimary into "diesel V6 torque monster"

Refuse those. If you show EngineHP, DisplacementL, or FuelTypePrimary from other sourced fields, show each on its own row. Never invent performance arithmetic in the cylinders mapper.

export function assertNoCylPerformance(moduleSource: string): void {
  const banned = [
    /v8 power/i,
    /0-60/i,
    /tows like/i,
    /muscle/i,
    /sporty four/i,
    /torque monster/i,
  ];
  for (const re of banned) {
    if (re.test(moduleSource)) {
      throw new Error(
        `cylinders module must not invent performance: ${re}`,
      );
    }
  }
}

export type CardRow = { label: string; value: string };

export function cardRows(fields: {
  EngineCylinders?: string | null;
  EngineHP?: string | null;
  DisplacementL?: string | null;
}): CardRow[] {
  const view = viewEngineCylinders(fields);
  const out: CardRow[] = [];
  if (view.source === "missing") {
    out.push({
      label: "Engine cylinders",
      value: "not provided",
    });
  } else if (view.cylinders != null) {
    out.push({
      label: "Engine cylinders",
      value: String(view.cylinders),
    });
  } else {
    out.push({
      label: "Engine cylinders",
      value: String(view.raw),
    });
  }

  const hp = (fields.EngineHP ?? "").trim();
  if (hp && !EMPTY.has(hp.toLowerCase())) {
    out.push({ label: "Engine HP (vPIC)", value: hp });
  }
  const displ = (fields.DisplacementL ?? "").trim();
  if (displ && !EMPTY.has(displ.toLowerCase())) {
    out.push({ label: "Displacement L (vPIC)", value: displ });
  }
  return out;
}
Enter fullscreen mode Exit fullscreen mode

Keep HP and displacement on their own labeled rows. Never concatenate cylinders with liters into "V8 muscle."

UI copy that stays honest

Prefer:

  • "Engine cylinders (vPIC): 6"
  • "Engine cylinders: not provided by vPIC for this VIN"
  • A short footnote: catalog count, not a performance claim

Avoid:

  • "V8 Power Package verified"
  • "Sporty I4 -- zippy commute"
  • Grey placeholders that look like real cylinder data when the field was empty ## Quick checks
import assert from "node:assert/strict";

assert.equal(viewEngineCylinders({}).source, "missing");
assert.deepEqual(
  engineCylindersLines(viewEngineCylinders({})),
  ["Engine cylinders: not provided by vPIC for this VIN"],
);

const six = viewEngineCylinders({ EngineCylinders: "6" });
assert.equal(six.cylinders, 6);
assert.ok(
  engineCylindersLines(six).some((l) =>
    /not a horsepower or tow/i.test(l),
  ),
);
assert.ok(
  !engineCylindersLines(six).some((l) =>
    /v6|muscle|0-60|tows/i.test(l),
  ),
);

const rows = cardRows({
  EngineCylinders: "",
  EngineHP: "280",
  DisplacementL: "3.5",
});
assert.ok(
  rows.some(
    (r) =>
      r.label.includes("cylinders") && r.value === "not provided",
  ),
);
assert.ok(rows.some((r) => r.label.includes("Engine HP")));
assert.ok(rows.some((r) => r.label.includes("Displacement")));
assert.ok(!rows.some((r) => /muscle|V8 power/i.test(r.value)));
Enter fullscreen mode Exit fullscreen mode

Review rule: cylinder modules must not contain performance marketing phrases except in forbidding tests. Never upgrade a blank into "V6."

Takeaway

EngineCylinders is a catalog count. Display it with clear sourcing, allow "not provided," and never use it as a key into invented horsepower, layout letters, or tow claims. Your free VIN UI stays trustworthy when cylinder count is either a real vPIC value with a modest footnote -- or absent -- and performance marketing lives somewhere else, clearly labeled, or not at all.

I maintain VIN Lookup, a free VIN decode based on NHTSA data.

Top comments (0)