Skip to content

Instantly share code, notes, and snippets.

@copernicus365
Last active October 1, 2024 06:58
Show Gist options
  • Save copernicus365/385d62f1e26247d1d78e70bd98c56fc8 to your computer and use it in GitHub Desktop.
Save copernicus365/385d62f1e26247d1d78e70bd98c56fc8 to your computer and use it in GitHub Desktop.
Class to get the subdomain from the domain / host of a Uri.Host string if it exists
namespace DotNetXtensions.Uri;
/// <summary>
/// `Uri` class does not allow getting a subdomain from the `uri.Host`.
/// This class provides a highly efficient method of getting a subdomain
/// if it exists on an input host / domain string (ideally sent in via
/// <see cref="Uri.Host"/>).
/// </summary>
public class UriHostWithoutSubdomain
{
/// <summary>
/// If subdomain detected returns the subdomain, else returns input value.
/// If null returns null.
/// </summary>
/// <param name="host">Send in `Uri.Host`</param>
public static string GetHostWithoutSubdomain(string host)
{
if(host == null)
return null;
if(!HasSubdomain(host, out int registrableDomainIndex))
return host;
string regDomain = host.Substring(registrableDomainIndex);
return regDomain;
}
/// <summary>
/// Detects if an input host string contains a subdomain. Is a highly efficient
/// implementation: with NO allocations, and with two for loops sharing a single `int i`:
/// one up to the first period, and one that continues from that point
///
/// and is NOT a validator of a domain string. For efficiency purposes
/// that is not desirable, as the intended use-case as well as separation of concerns
/// is that a `Uri.Host` value was input, or something like that.
/// Relying on this allows us to provide this highly efficient algorithm, which essentially
/// only needs to walk once through the string chars looking first for the first period,
/// and then, if a second period is detected, it is established that a subdomain exists.
/// If so, the out arg allows one to know where to take a substring to get the subdomain,
/// or the second-level domain without it, etc.
/// <para />
/// </summary>
/// <param name="host">Uri host / domain string</param>
/// <param name="postSubdomainIndex">If subdomain exists, this will be set to the
/// index at which the 'second-level domain' begins (after the subdomain and 1 AFTER the period,
/// so in foo.example.com, index will be at 'e' in 'example.com').
/// But if no subdomain (if FALSE), then this will be the position of the 'top-level' domain,
/// e.g. at the 'c' in "com" in "example.com". If one wants they could use this to
/// efficiently get the top-level domain and etc.</param>
public static bool HasSubdomain(string host, out int postSubdomainIndex)
{
ArgumentNullException.ThrowIfNull(host);
int i = 0;
int len = host.Length;
for(; i < len; i++)
if(host[i] == '.')
break;
if(i >= len) {
// no period at all
postSubdomainIndex = 0;
return false;
}
postSubdomainIndex = ++i;
for(; i < len; i++)
if(host[i] == '.')
break;
if(i >= len) {
// no second period was found, so period was for TLD (top-level domain)
return false;
}
// second period WAS found, and we are NOT at the end yet,
// but we do NOT need to go further now, subdomain ends before first period,
// we're now already at second
return true;
}
}
public class UriHostWithoutSubdomainTests
{
[Fact] public void NoPeriodAtAll() => _run("example", null);
[Fact] public void NoSub1() => _run("example.com", null, 8);
[Fact] public void Sub1() => _run("sb.example.com", "example.com", 3);
[Fact] public void Sub2() => _run("domain.gov.uk", "gov.uk", 7);
[Fact] public void WWWAsSub() => _run("www.navilavi.com", "navilavi.com", 4);
[Fact] public void EmptySubDom1() => _run(".example.com", "example.com", 1);
//
void _run(string domain, string host = null, int expectedIndex = -1)
{
string expected = host ?? domain;
string val = UriHostWithoutSubdomain.GetHostWithoutSubdomain(domain);
Assert.True(val == expected);
if(expectedIndex >= 0) {
bool hasSubdomain = host == domain;
bool hasSub = UriHostWithoutSubdomain.HasSubdomain(domain, out int _index);
Assert.True(_index == expectedIndex);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment