UFP (User Friendly Pattern Format) is a small open pattern language for extracting named values from text, together with its formal specification and reference implementations in several languages. A pattern is simply an example of the text with the changing parts replaced by names in angle brackets:
value: 04-05-2024_scan.jpg
pattern: <Date>_<Name>.<Extension>
result: Date = "04-05-2024", Name = "scan", Extension = "jpg"
There is no escaping, no character classes and no quantifiers - the language is deliberately much smaller than a regular expression. The format is defined by the open specification UFP-1.2 and is implemented in C# (.NET 10 and .NET Standard 2.0), JavaScript, PHP and Python.
UFP is meant for the case when the pattern is written by the end user: a doctor naming scans, an accountant splitting invoice numbers, a mechanic reading part codes. It is a focused alternative to regular expressions and to URI Templates (RFC 6570) for the common task "split this text into named fields".
Writing patterns yourself? Start with the user guide - it takes about five minutes. Integrating UFP into an application or porting it to another language? Read the specification, take the parser file for your language from the table below, and verify it with the conformance suite.
Want to try it first? The live demo parses the text under a pattern as you type and shows the captured variables, with nothing to install.
| Regular expressions | URI Templates (RFC 6570) | UFP | |
|---|---|---|---|
| Written by a non-programmer | Hard: escaping, groups, quantifiers | Works only for URL-shaped text | The pattern looks like an example of the text |
| Named extraction | Extra (?<Name>...) syntax |
Expression names | The name is the slot: <Name> |
| Arbitrary static text around values | Needs quoting and escaping | Limited | Any character, no escaping |
| Several shapes in one pattern | Alternation plus anchors | No | Alternatives separated by | |
| Greedy captures | .* vs .*? and backtracking |
Prefixes and operators | One * before > |
| Deterministic result | Depends on the engine | Yes | Fully specified |
| Worst case | Can be catastrophic (ReDoS) | - | Linear, no backtracking |
Key properties:
- Readable by end users. A pattern is written as one real example with holes:
<Date>_<Name>.<Extension>. No manual, no escaping rules to remember. - The pattern is the schema. Capturing happens by name, so the result is immediately usable structured data.
- No escaping. Every character except
<,>,|is a literal - spaces, dots, slashes, Cyrillic, Arabic, Hebrew and emoji are all fine. - Several shapes in one pattern. Alternatives are part of the syntax:
IMG_<Number>.jpg|scan_<Number>.jpgreplaces several separate checks, and the alternative that captured the most names wins. - No regex under the hood. The pattern is not translated into a regular expression; it is tokenized into a small matching plan and matched with ordinary substring searches.
- Linear and predictable. Separators are located with ordinary substring searches; there is no recursion and no backtracking, so a hostile pattern cannot hang the parser.
- Never fails.
Parsealways returns a map: a mismatch gives an empty map or the variables captured before it. Every conforming implementation returns the same result for the same input. - Tiny and dependency-free. Each implementation is a single source file with no third-party dependencies, and every one of them is verified against the same conformance suite.
| Document | For whom |
|---|---|
| User guide | End users - doctors, accountants, mechanics: how to write a pattern in five minutes, with simple examples |
| Specification (Russian translation) | Implementers: grammar, primitive operations, the complete algorithm in language-neutral pseudocode, failure semantics, edge cases, conformance requirements |
| Conformance suite | Anyone writing or checking a port: 77 machine-readable vectors in spec/ufp-1.2.test-vectors.json |
| Changelog | History of the format and of the parsing algorithm |
| Contributing | How to build, test and propose changes |
| Element | Meaning | Example |
|---|---|---|
<Name> |
A variable: captures the text at this place and stores it under Name |
<Date>, <Patient.FirstName> |
text |
A static part (separator): literal text that must be present | _, .jpg, IMG_ |
<Name*> |
A greedier variable: captures as much as possible up to the last following separator | <Path*>/<Filename>.<Extension> |
A|B |
A pattern set ("mega pattern"): the alternative that captures the most variables wins | IMG_<Number>.jpg|scan_<Number>.jpg |
Rules in one screen:
- A name is anything between
<and>, trimmed at the edges and case-sensitive. The same name may be reused; the last captured value wins. - Every variable that captures text is returned: there is no anonymous or discard syntax, and
<>is the empty name, not a way to hide a value. If a value is not interesting, give it a name and ignore it in the code. - A static part may be any text without
<,>,|. It is matched exactly, including spaces. - A static part at the beginning of a pattern must match the beginning of the value and is not captured:
IMG_<Number>.jpggivesNumber = "1234"forIMG_1234.jpgand an empty result forscan_99.jpg. - Two variables cannot stand next to each other: in
<A><B>the first one is ignored. - The last variable captures the rest of the value, including text that looks like a separator.
- Text direction does not matter: right-to-left scripts such as Arabic and Hebrew are matched in logical order.
- The result is always a map of strings. Values are never trimmed, converted or validated -
"10"stays"10". - A pattern that fits only partially returns the variables captured before the first mismatch; an empty map means that the value does not have the shape described by the pattern.
The full rules, including edge cases, are in the specification and in the user guide.
pattern: <Path*>/<Filename>.<Extension>
value: docs/reports/2024/annual_report.pdf
result: Path = "docs/reports/2024", Filename = "annual_report", Extension = "pdf"
Variable names are stable keys, not just throwaway labels for one file. An application that knows its patterns can use them as a small schema:
- Explain the fields. Show the user which values a pattern captures and what each one means:
<Patient.FirstName>can be displayed as "First name" or "Имя", while the pattern keeps the technical key. - Build patterns in a UI. Offer a palette of known variables and let the user assemble a pattern by drag and drop instead of typing angle brackets by hand.
- Share one vocabulary. The same names are used by the parser, by the UI labels and by the code that consumes the result, so a pattern doubles as configuration and as documentation of the data.
Names may also be completely free-form when the end user writes patterns for their own files - the format does not care.
Every implementation is a single dependency-free source file that passes the conformance suite. Copy the file into your project (or reference the project) - there is nothing to install.
| Language | Platform | Parser file | Entry point |
|---|---|---|---|
| C# | .NET 10 (net10.0) |
src/UFP.NET/UfpParser.cs |
UFP.NET.UfpParser.Parse |
| C# | .NET Standard 2.0 (netstandard2.0, C# 7.3) - .NET Framework 4.6.1+, UWP, Unity, Xamarin and other legacy targets |
src/UFP.NET.Standard/UfpParser.cs |
UFP.NET.Standard.UfpParser.Parse |
| JavaScript | Node.js 20+, ES module | src/UFP.JavaScript/ufp.js |
parse |
| PHP | PHP 8.0+ | src/UFP.PHP/ufp.php |
UFP\parse |
| Python | Python 3.6+ | src/UFP.Python/ufp.py |
parse |
Parse / parse always returns a map (dictionary, object, array or dict) of the captured variables and never throws: a value that does not fit gives an empty map (or the variables captured before the mismatch), and null/None arguments give an empty map as well. An empty map means that the value does not have the shape described by the pattern.
Use UFP.NET on .NET 10, and UFP.NET.Standard (identical API) on any older target:
using UFP.NET; // or UFP.NET.Standard
using System.Collections.Generic;
const string pattern = "<Date>_<Name>.<Extension>";
Dictionary<string, string> pairs = UfpParser.Parse("04-05-2024_scan.jpg", pattern);
// pairs["Date"] = "04-05-2024"
// pairs["Name"] = "scan"
// pairs["Extension"] = "jpg"import { parse } from "./src/UFP.JavaScript/ufp.js";
const pattern = "<Date>_<Name>.<Extension>";
parse("04-05-2024_scan.jpg", pattern);
// { Date: "04-05-2024", Name: "scan", Extension: "jpg" }require_once "src/UFP.PHP/ufp.php";
$pattern = "<Date>_<Name>.<Extension>";
UFP\parse("04-05-2024_scan.jpg", $pattern);
// ["Date" => "04-05-2024", "Name" => "scan", "Extension" => "jpg"]A variable name that is a canonical integer (for example <0>) becomes an integer array key, because PHP casts such keys; the captured pairs are the specified ones.
from ufp import parse # ufp.py copied into your project or on sys.path
pattern = "<Date>_<Name>.<Extension>"
parse("04-05-2024_scan.jpg", pattern)
# {'Date': '04-05-2024', 'Name': 'scan', 'Extension': 'jpg'}The ports also export their tokenizer and the primitive string operations (tokenize, find, rfindWithin / rfind_within) for tests and for other ports.
The format is defined by docs/SPECIFICATION.md (Russian translation): the grammar, the primitive string operations, the complete algorithm in language-neutral pseudocode, failure semantics, an edge-case catalogue and the requirements for a conforming implementation. The current version is UFP-1.2 (published 2026-11-04). The specification is a dated public disclosure under the MIT license, so the format stays free for every implementation, including proprietary ones.
Every implementation is verified against spec/ufp-1.2.test-vectors.json - 77 vectors, each with the exact map that Parse must return. The format of the file and the way to run it against your own implementation are described in spec/README.md:
dotnet test UFP.slnx --configuration Release --filter "FullyQualifiedName~UfpConformanceTests" # .NET
py src/UFP.Python/ufp.py # Python
node src/UFP.JavaScript/conformance.js # JavaScript
php src/UFP.PHP/conformance.php # PHP
# .NET reports 77 passed conformance tests; each port runner prints: vectors: 77, failures: 0The format version is independent of the version of any implementation: ports can be added, rewritten or updated without changing UFP-1.2, as long as they keep passing the suite. The format itself is intentionally closed and small: it is not going to grow into a regular-expression engine, and its syntax is not expected to change.
Mean time and memory per single Parse call. Scenarios: Single = <Date>_<Name>.<Extension>, MegaPattern = three alternatives in one pattern, GreedyVariables = <Path*>/<Filename>.<Extension>. Same machine (AMD Ryzen 7 7800X3D 4.20 GHz, Windows 10 22H2, 2026-11-04); BenchmarkDotNet 0.15.8 for .NET, the standard-library timers of each language for the ports (see benchmarks/).
| Implementation | Single |
MegaPattern |
GreedyVariables |
|---|---|---|---|
.NET 10 (net10.0) |
146.3 ns / 440 B | 342.6 ns / 664 B | 146.3 ns / 464 B |
| .NET Standard 2.0 | 164.2 ns / 864 B | 428.7 ns / 2.33 KB | 162.5 ns / 896 B |
| JavaScript (Node.js 24) | 191.2 ns / 416 B | 512.8 ns / 1.16 KB | 182.4 ns / 416 B |
| Python 3.15 | 709.9 ns / 361 B | 1664.2 ns / 387 B | 697.0 ns / 372 B |
| PHP 8.3 | 1167.4 ns / 488 B | 2613.5 ns / 504 B | 1037.5 ns / 496 B |
The numbers are steady state: the tokenized pattern and the compiled matching plan are cached, so parsing many values with the same pattern does not pay for compilation again. Memory is the memory allocated by one call (retained memory for PHP).
The modern .NET build is the fastest and the most allocation-light; .NET Standard 2.0 is 11-25% slower and allocates 1.9-3.6x more memory, the price of staying C# 7.3-compatible on netstandard2.0 without the newer span APIs. JavaScript is within 1.5x of .NET 10, Python is about 5x and PHP 7-8x slower. A typical single-pattern parse stays between 0.15 and 1.2 microseconds in every implementation, and all of them allocate only the result map, the captured values and a small positions array.
| Path | Contents |
|---|---|
src/ |
One dependency-free parser per language |
spec/ |
Conformance vectors and instructions for running them |
docs/ |
Specification (English and Russian) and the end-user guide |
tests/ |
Unit tests and conformance runners for every parser |
benchmarks/ |
Benchmark projects for every parser |
playground/ |
A small .NET console app that prints the parsing result for a few patterns and values, and HTML live demo page |
Any contributions are welcome - please read CONTRIBUTING.md first. The list of changes is kept in CHANGELOG.md.
This project is licensed under the MIT License - see the LICENSE file for details.