Skip to content

Latest commit

 

History

32 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Loin-c11

A small program to browse the LOINC catalogue: 112,405 laboratory and clinical test codes, in two files of its own design. It links no library — not a database, not a toolkit — and reads its catalogue with <stdio.h>.

The name is the whole project in one word: LOINC, the catalogue it browses, and C11, the standard it is written to. The command you type is still loinc.

This is a study in C. It was written from scratch to learn the language, one concept at a time, each one compiled and run before the next was started. The program is real and does its job, but the point of it was always the road rather than the destination — so the reasoning is written down where it belongs: in the comments, which explain why far more often than what, and in STYLE.md, CONVENTIONS.md and VERSIONING.md, which say what the rules are and how they were arrived at.

The § numbers scattered through the source point into a learning journal kept alongside the project and not published here. Nothing in the code depends on it: every comment that cites a section also says what happened in it.

If you are learning C too, read the comments. They are half the repository.

It works two ways. Run it with no arguments and it shows a menu; give it arguments and it does one job, prints the answer and exits — so it fits in a pipeline like any other Unix tool.

$ loinc search glucose | head -3
100746-7   Glucose [Moles/volume] in Mixed venous blood
104597-0   Glucose [Mass/volume] in Venous blood by Glucometer
104598-8   Glucose [Mass/volume] in Arterial blood by Glucometer

It works three ways, in fact. There is also a window:

The Loin-c11 window

All three — the menu, the command line and the window — sit on the same engine and share the same object files. None of them is a wrapper around another.


Requirements

  • a C11 compiler (cc, i.e. gcc or clang)
  • make
  • optional, for make gui: GTK 3 — sudo apt install libgtk-3-dev
  • optional, for make format: clang-format

The program itself needs no library at all: it stores the catalogue in two files of its own design and reads them with <stdio.h>. That was a deliberate trade — it gave up a query language, transactions and a mature index in exchange for knowing exactly what every byte on disk is for, which is the better bargain for a program whose purpose is to be understood.

Build

make            # build the CLI (debug: -Wall -Wextra -Werror, asan, ubsan)
make run        # build, then run from here so data/loinc.dat resolves
make gui        # build the GTK 3 program — needs libgtk-3-dev
make run-gui    # build the GUI, then run it
make tools      # build/flatsearch, the oracle behind the binary search
make check      # check the program against the release CSV
make format     # reformat sources with clang-format
make clean      # delete build/

make builds build/loinc, which links nothing: no toolkit, no database library, just libc. make gui builds build/loinc-gui, the same engine with GTK 3 on top. Both run on a machine with no display, except the one that needs a display.

Warnings are errors and the sanitizers are always on: if it builds and runs without a word, it is clean.

Checking it

make check        # or ./tools/check.sh path/to/LoincTableCore.csv

Thirteen checks against the release CSV: the record count, all 440 class groupings with their counts, every field of four whole records, and seven searches. The CSV is parsed by python's csv module rather than by ours — a checker that shares the suspect's parser cannot catch a bug in it — and everything after that is awk, sort and cmp over the program's --tabs output. It reads only; nothing is written.

The catalogue

The catalogue is two files, relative to the current directory:

data/loinc.dat    one fixed-size record per item, sorted by COMPONENT
data/loinc.txt    the text those records point at

So run the program from loinc-c/, or from any directory with a data/ in it. There is no configuration file and no option to point it elsewhere yet.

cd loinc-c && ./build/loinc classes    # works
cd /tmp    && /path/to/loinc-c/build/loinc classes
db_open: cannot open data/loinc.dat and data/loinc.txt   # exit 3

If data/ exists but the files do not, an empty catalogue is created — which is why loinc import works on a fresh checkout.

Editing and importing write to those files. To try either safely, work on a copy:

mkdir -p /tmp/loinc-test/data
cp data/loinc.dat data/loinc.txt /tmp/loinc-test/data/
cd /tmp/loinc-test && "$OLDPWD/build/loinc"

The two files carry the same generation number in their headers, and the program refuses a pair that does not match — copy both, or neither.


Command mode

loinc                          interactive menu
loinc search [field] <term>    items whose field starts with <term>
loinc classes [0-4]            classes of one type, with their item counts
loinc show <loinc_num>         one record, field by field
loinc import [--dry-run] <csv> replace the table with a CSV release
loinc help                     the summary above (also -h, --help)
loinc --tabs <command> ...     TAB-separated output, for cut and awk

search [field] <term>

Finds every item whose chosen field starts with <term>, case-insensitive, ordered by COMPONENT. One line per item: the LOINC number and the long common name. field is optional and comes before the term:

field column example
component (default) COMPONENT loinc search glucose
number LOINC_NUM loinc search number 14749
shortname SHORTNAME loinc search shortname Hgb

A term with spaces needs quotes, or the shell hands us extra arguments and we report a usage error rather than searching for the first word only:

$ loinc search glucose in blood
Usage: loinc search [field] <term>          # exit 2

$ loinc search "glucose in blood"           # exit 1, nothing starts with that

classes [0-4]

Lists the distinct CLASS values with the number of items in each. The optional digit is the LOINC class type, and it narrows which classes are listed — it does not filter items:

digit meaning classes
0 (default) every class 440
1 Laboratory 74
2 Clinical 172
3 Claims attachments 25
4 Surveys 169
$ loinc classes 3
ATTACH                       11
ATTACH.AMB                   55
ATTACH.CARD                   1

show <loinc_num>

Prints every column of one item, one NAME value line per column.

$ loinc show 14749-6
LOINC_NUM                  14749-6
COMPONENT                  Glucose
PROPERTY                   SCnc
TIME_ASPCT                 Pt
SYSTEM                     Ser/Plas

import [--dry-run] <csv>

Replaces the whole table with a LOINC release in CSV form — the LoincTableCore.csv of an official download, or any RFC 4180 CSV whose header names the table's columns in the table's order.

$ loinc import --dry-run Loinc_2.83/LoincTableCore/LoincTableCore.csv
Checking '…/LoincTableCore.csv'...
112406 lines read, 112405 records.
Dry run: nothing was written.

$ loinc import Loinc_2.83/LoincTableCore/LoincTableCore.csv
Importing '…/LoincTableCore.csv'...
112406 lines read, 112405 records.
98269 rows removed, 112405 rows written.

Three things worth knowing:

  • The header is checked, not skipped. The table is asked for its own column names and they must match, in order. A file that does not fit is refused before anything is written, naming the column that disagrees:

    $ loinc import wrong.csv
    import_csv: column 4 of the CSV is "TIME_ASPECT", but the table calls it "TIME_ASPCT".
    Import failed after 1 lines. The table was not changed.
  • It is all one transaction. The delete and every insert live inside a single BEGIN/COMMIT. Any failure — a malformed line halfway through, a disk error — rolls the whole thing back and leaves the old catalogue exactly as it was. There is no state in which you have half a table.

  • --dry-run never opens a transaction. It parses the file, checks the header, counts the records and reports every malformed line, then stops. Use it on a release you have not seen before. It exits 3 if the file has any malformed line, even though nothing was written.

The importer does not create the table: it replaces the contents of one that already exists. See Starting from an empty database below.

Fields may contain commas and quotes; "" inside a quoted field means one ". CRLF line endings are handled. A newline inside a field is the one CSV feature not supported — such a line is reported by number and refused, never silently mangled.


Starting from nothing

This repository holds no data: LOINC is redistributed under its own terms, and a release is 26 MB of CSV. Two steps:

  1. Download a release from https://loinc.org/downloads/ (free account) and unpack it. You want LoincTableCore/LoincTableCore.csv.

  2. Import it:

    mkdir -p data
    ./build/loinc import --dry-run path/to/LoincTableCore.csv   # look first
    ./build/loinc import path/to/LoincTableCore.csv

That is all. There is no table to create and no database to prepare: the program makes an empty catalogue the first time it opens one that is not there, and the import fills it. It writes data/loinc.dat (fixed-size records) and data/loinc.txt (the text they point at).

The importer checks the CSV header against the fifteen columns it expects, in order, and refuses a file that does not match, naming the column that disagrees. Nothing is written until the whole file has parsed.

Output and exit codes

Data goes to stdout, everything else to stderr. Prompts, banners, counts and errors never pollute the output, so 2>/dev/null gives you clean data and a pipeline keeps working.

$ loinc classes 2>/dev/null | sort -k2 -rn | head -3
MICRO                     16109
CHEM                      10872
DRUG/TOX                  10194

Output for programs: --tabs

The columns above are padded so they line up on a screen, which makes them a picture of a table rather than a table: five class names contain a space (H&P.SURG PROC among them), so awk '{print $2}' reads the second word of the name instead of the count.

--tabs, before the command, separates every field with exactly one TAB and pads nothing:

$ loinc --tabs classes | awk -F'\t' '{s+=$2} END {print s}'
112405

$ loinc --tabs show 14749-6 | awk -F'\t' '$1=="SYSTEM"{print $2}'
Ser/Plas

It applies to search, classes and show. There is no auto-detection on purpose: a program that behaves differently depending on whether a terminal is attached is a program whose script works by hand and breaks in cron.

The exit code says what happened, in the shape of grep's:

code meaning
0 the command ran and found something
1 nothing matched — an answer, not a failure
2 the command line was wrong (unknown command, missing or extra argument, bad field or class type)
3 the database or a query failed
if loinc show 14749-6 >/dev/null 2>&1; then
    echo "that code exists"
fi

Note that loinc … | head -3 exits 141 (128 + 13, SIGPIPE): head closed the pipe and the kernel stopped us. That is what every Unix tool does, and it is why command mode never paginates — stopping is the reader's job.

Recipes

# how many items start with "hemoglobin"
loinc search hemoglobin 2>/dev/null | wc -l

# just the codes
loinc search insulin 2>/dev/null | cut -d' ' -f1

# one field of one record
loinc show 14749-6 2>/dev/null | grep '^SYSTEM'

# show the first hit of a search
loinc show "$(loinc search cholesterol 2>/dev/null | head -1 | cut -d' ' -f1)"

# the laboratory classes, biggest first
loinc classes 1 2>/dev/null | sort -k2 -rn | head

Interactive mode

Run loinc with no arguments:

=== The LOINC Table Core ===
  1) Search items
  2) List classes
  3) Show a record
  4) Edit a record
  5) Import a CSV release
  0) Quit
Choice:

The menu asks for what command mode takes as arguments — the search field, the class type — and an empty line always keeps the default shown in brackets.

4) Edit a record has no command-mode equivalent, because editing is a conversation: it walks every field of a record showing the current value, and an empty line keeps it. Nothing is written until you answer y to the final confirmation. The record is identified by the LOINC number you loaded it with, so you can change the LOINC number itself and the row is renamed rather than lost.

Ctrl-D (EOF) means "quit" everywhere, so the program can be driven by a script:

printf '2\n0\n0\n' | loinc 2>/dev/null      # list classes, all types, then quit

Graphical mode

make gui && ./build/loinc-gui           # or: make run-gui
./build/loinc-gui glucose               # open with the answer already there

A third frontend on the same engine — include/db.h does not know it exists, and no GTK type appears anywhere in the engine. GTK 3 is the only library the project links anywhere, and only this one program links it.

Pick a field, type a term, press Enter or Search, and the list fills — the same query as loinc search [field] <term>, wildcards included, out of the same table of fields the command line parses. Clear empties the list and the term (and leaves the chosen field alone).

The list shows all seven fields the engine hands over per item. Columns can be dragged wider and clicked to sort; the sort happens in memory, so it is instant on any column — while the file itself is ordered by COMPONENT and nothing else, which is why searching SHORTNAME costs a scan.

A single click selects a row and wakes Edit…; a double click opens it straight away. The editor is a modal dialog with every field of the record and a Save that writes nothing until you press it. CLASSTYPE, STATUS and SCALE_TYP offer their documented values in a drop-down — one you can still type into, because the catalogue is allowed to contain values we have not heard of. Closing the dialog puts the list back where you left it, on the row you were working on.

There is no limit and no paging: the scrollbar is this frontend's head.

The status bar has four sections, so they cannot overwrite each other: which LOINC release this catalogue is, which record is selected, what just happened, and the date and time. The release is derived from the data rather than published with it — the tooltip says so — and an em dash means the catalogue was imported before the program started recording it.

File imports a release — a dry run first, then a question naming the record count, because an import replaces the whole catalogue — shows the logbook, and rewrites the catalogue to reclaim the space edits leave behind. All three run on a worker thread, so the window (and the clock) keep going. Help says who wrote it, and where in the galaxy.

What it does not do yet: filter the classes by type, or draw anything.

One thing worth knowing if you plan to change it: a window cannot be driven by a pipe, so the automatic gate that guards everything else here does not reach it. That is the reason the src/ui_*.c files contain no logic at all — whatever is worth testing lives in the engine, where make check can see it.


Notes on the data

The catalogue here is LOINC release 2.83: 112,405 items in 440 classes, loaded with loinc import from the official LoincTableCore.csv.

Every field is text. There are no types to speak of: a record is bytes, and include/flat.h says how wide each field is and nothing else. CLASSTYPE holds the strings "1" to "4" for the same reason the CSV does — nobody ever converted them, because there is nothing to convert them to.

The widths come from the data and are therefore a promise about a particular release. CLASS was 24 bytes wide until 2.83 arrived with a 25-character class name; it is 32 now. When a width changes, sizeof(FlatRecord) changes, and the header of an older data/loinc.dat no longer matches — the program refuses the file rather than reading it at the wrong offsets. Re-import after any such change.

Two historical notes, in case an old catalogue or an old comment turns up. The catalogue lived in a database until §28 — that engine was removed in §43, and is still in the history — and the file it lived in had the CSV header imported as a data row years ago — which is why the count used to be 98,269 rather than 98,268, why classes used to list a class called CLASS, and why loinc show LOINC_NUM used to print a record made of column names. loinc import cannot reproduce that: it consumes the header to check it, so the header can never reach the data. One import, and the wart was gone.

Where things are

loinc-c/
  README.md         this file — how to use it
  LICENSE           MIT
  STYLE.md          how a line of code is written
  CONVENTIONS.md    how the program is designed
  VERSIONING.md     the three version numbers, and which is which
  Makefile
  include/db.h      the engine's public interface: query and edit
  include/import.h  the engine's public interface: load a CSV release
  include/flat.h    the fixed-record file format the engine is built on
  include/csv.h     the RFC 4180 parser
  include/fields.h  the searchable fields, shared by every frontend
  include/logbook.h what the program writes down about itself
  include/version.h the program's version, defined once
  src/db_flat.c     the engine: db.h on two files, and nothing about the user
  src/import_flat.c import_csv: a CSV release straight into those files
  src/flat.c        the format itself: offsets, header, text lookups
  src/csv.c         src/fields.c  src/logbook.c
  src/flat_internal.h private to the engine — the real `struct Db`
  src/main.c        the frontend: menu, command mode, printing
  src/ui.h          private to the window — its shared `struct Ui`
  src/ui_main.c     the window: open the catalogue, build it, hand over
  src/ui_search.c   ask the engine a question   src/ui_table.c   the list
  src/ui_classes.c  the classes down the left   src/ui_record.c  the editor
  src/ui_logbook.c  read the logbook back       src/ui_task.c    long jobs
  src/ui_menu.c     the menu bar, About, and leaving
  src/ui_status.c   the message line and the clock
  src/flatsearch.c  the same search over those files, and the oracle that
                    guards the binary search
  data/loinc.dat    the records            } the catalogue: not in the
  data/loinc.txt    the text they point at  } repository, built by `import`
  docs/screenshot.png

Not there yet

The graphical mode searches and shows records; browsing the classes, editing and importing are still menu-and-command-line only, and its search runs on Enter rather than as you type.

Editing is menu-only by design — it is a conversation, so there is no reusable half. The importer replaces the whole catalogue rather than merging into it. show scans, because the catalogue is ordered by COMPONENT and nothing else: a second index is the obvious next thing the storage wants.

License

MIT — see LICENSE. Deliberately permissive: this is teaching material, and material you have to ask permission to learn from teaches less. Take the CSV parser, take the ownership patterns, take whatever is useful.

LOINC itself is a registered trademark of Regenstrief Institute, Inc., and its content is subject to the LOINC licence. None of it is distributed here — this repository contains no data.

About

A study in C11: a LOINC catalogue browser with three frontends, its own file format, and a journal of every decision

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages