I am trying to create the ultimate regular expression to retrieve the version number from a file name. I know this question has already received some answers elsewhere on StackOverFlow (like here, here or here), but they do not provide a regular expression to handle all the edge cases. I want to write the ultimate regular expression to get the version number, the delimiter used and the stage (if one). For now this regex meets my needs but I'm almost sure I'm forgetting very special cases that didn't appear during my tests or that I wouldn't have thought of. I need my regex to be as resilient as possible. What are the cases that I could have forgotten and how to improve my regex as much as possible? You will find the current status of my work below and a demonstration here. Here are some examples of the cases I am currently handling: # Version is 1.93.1 filename_arm64.1.93.1-1726078001_armhf.deb # No version filename_cli_alpine_arm64.tar.gz # Version is 1-94-10009-67 and stage is stable filename-x86_1-94-10009-67-stable_filename.exe # Version is v1.94.10009 filename-v1_94_10009.67-arm64_filename.exe # version is 1.93.1 filename-win32-x64-1.93.1.7z # Version is 2024-09-01 stage is rc40 filename_2024-09-01-rc40.exe I will put the full regex that I came up with at the very end of the message because it can be a bit long. I am explaining for the moment the following example: (??:win32|win64)(?:\.|-|_)?)?(v?\d+(\.|-|_)?(?:\d+\2){1,2}(?!7z)\d+)(??(?:amd64|arm64)?([A-Za-z]+\d*)? This regex captures three groups: the core version, the delimiter and the stage. Capture the core of the version number The main one is v?\d+(\.|-|_)?(?:\d+\2){0,2}(?!7z)\d+ and consists of five parts. v? matches the character v (or not) at beginning of the version number. I want to keep it, but you can get rid of it if you want. \d+(\.|-|_)? matches a group of several digits followed or not by a delimiter (captured in a group). Delimiters here are the dot ".", the hyphen "-" or the underscore "_". (?:\d+\2){1,2} matches groups of several digits separated by and ending with the same delimiter as the one captured previously. Note that in this example {1,2} limit the number of groups of digits composing the version number from 2 to 4. (?!7z) does not match the last digit if it is a "7" followed by the letter "z" This part is used to handle the edge case of a filename's stem ending with the version number followed by the extension ".7z". \d+ match the last group of digits (with no delimiter at the end). Handle non-stable versions The (-?)(?:<pattern>|<pattern>)?([A-Za-z]+\d*)? part following allows to capture the stage of non-stable versions attached directly to the version number or by a hyphen. A stage is considered to be a word followed, or not, by a number. Examples: "Alpha", "Beta", "rc3"... Unfortunately this regular expression generates false positives. This is why the part (?:<pattern>|<pattern>)? is added to ignore certain words. In this example "arm64" and "amd64" would be ignored. Edge cases The regular expression begins by (?:<pattern>|<pattern>)?(?:\.|-|_)?) to prevent specifics edge cases when strings finishing by digits (followed by a delimiter) introduce the version number. In this example "32" ansd "64" from "win32" and "win64" would not be part of the version number. The ultimate regex (??:arm64|amd64|win32|win64|x64|x86)(?:-|_|\.))?(v?\d+(-|_|\.)(?:\d+\2){1,4}\d+)(??(?:arm64|amd64|win32|win64|x64|x86|Mon|Tue|Wed|Thu|Fri|Sat|Sun|windows|linux|macos|DebugSymbols)?([A-Za-z]+\d*)? Another way to build it without overloading it would be to make it parameterizable. In python this would give something like: #-------- ALL THE CONSTANTS --------# # Keywords to ignore when guessing a version from a string ARCHITECTURES = ( "arm64", "amd64", "win64", "win32", "x64", "x86", ) DAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") OPERATING_SYSTEMS = ("windows", "linux", "macos") MISC = ("DebugSymbols",) KEYWORDS = (*ARCHITECTURES, *DAYS, *OPERATING_SYSTEMS, *MISC) KEYWORDS_ENDING_WITH_DIGIT = set(filter(lambda k: k[-1].isnumeric(), KEYWORDS)) # Delimiters that can separate the different numbers making up the version DELIMITERS = ".-_" # Patterns to add in the regex PATTERN_DELI = "|".join(DELIMITERS).replace(".", r"\.") PATTERN_KW = "|".join(KEYWORDS) PATTERN_KW_EWD = "|".join(KEYWORDS_ENDING_WITH_DIGIT) # Number of parts in the version MIN_ELEM = 2 MAX_ELEM = 6 #-------- THE CODE --------# mini = cls.MIN_ELEM - 1 if cls.MIN_ELEM > 1 else 0 maxi = cls.MAX_ELEM - 2 if cls.MAX_ELEM > 2 else 0 regex = rf"(??:{PATTERN_KW_EWD})(?:{PATTERN_DELI}))?(v?\d+({PATTERN_DELI})(?:\d+\2){{{mini},{maxi}}}(?!7z)\d+)(??(?:{PATTERN_KW})?([A-Za-z]+\d*)?" version_groups = re.search(regex, string) if version_groups is None: print(f'No version found in "{string}"') else: version, delimiter, stage = version_groups.groups() Continue reading...