IE's Extra Robust Color Parsing®

Recently, I've found a rant from 2004 about Internet Explorer's color parsing. Turns out, MS made sure that just about anything will be parsed as a color somehow and I think it makes for a neat challenge.

Well, how does IE parse colors?

Let's give it an input with a length of...

• ...less than 4 chars:
1. Replace chars not matching [0-9a-fA-F] with "0"
2. Append "0" until the length is 3 chars
3. Split into 3 parts, 1 char each
4. Prepend each part with "0"
• more or equal than 4 chars:
1. Replace chars not matching [0-9a-fA-F] with "0"
2. Append "0" until the length is divisible by 3
3. Split into 3 parts of equal size
4. Remove chars from the front of each part until they are 8 chars each
5. Remove chars from the front of each part until one of them starts with something that isn't "0" or until all parts are empty
6. Look at the length of the parts: Are shorter than 2? If so, prepend each part with a "0" until each part is 2 chars long. If not, remove characters from the back of each part until they are 2 chars each

... w h a t

It's true! Here are some examples (irrelevant steps omitted):

F
--> F00        // append "0" until length is 3
--> F, 0, 0    // split into 3 parts
--> 0F, 00, 00 // prepend 0
--> #0F0000    // done

FLUFF
--> F00FF      // replace invalid chars with 0
--> F00FF0     // pad to length that is divisible by 3
--> F0, 0F, F0 // split into 3 parts
--> #F00FF0    // done

--> 0AD00AC000E       // replace invalid chars with 0
--> 0AD0, 0AC0, 00E0  // split into 3 parts
--> AD0, AC0, 0E0     // remove "0"s from the front of each part
--> AD, AC, 0E        // remove chars from the back of each part until
// each part is of length 2

1234567890ABCDE1234567890ABCDE
--> 1234567890, ABCDE12345, 67890ABCDE // split into 3 parts
--> 34567890, CDE12345, 890ABCDE       // remove chars from the front until
// each part is of length 8
--> 34, CD, 89                         // remove chars from the back of
// each part until each part is of length 2
--> #34CD89                            // done

rules
--> 000E0      // replace invalid chars with "0"
--> 000E00     // pad to length divisible by 3
--> 00, 0E, 00 // split into 3 parts
--> 0, E, 0    // remove "0"s from the front of each part until one part
--> 00 0E 00   // parts are too short, prepend a "0"
--> #000E00    // done

GHIJKL
--> 000000      // replace invalid chars with "0"
--> 00, 00, 00  // split into 3 parts
--> , , ,       // remove "0" from the front of each part until empty
--> 00, 00, 00  // parts are too short, prepend "0" until they are length 2
--> #000000     // done


Rules

• This is , shortest answer wins
• No standard loopholes
• Expect any input matching [a-zA-Z0-9]*. Yes, empty input as well (results in #000000).
• Output any representation of the resulting RGB color (including but not limited to the actual color, tuples, hex codes, with or without #, any case, ...)
• For the sake of this challenge, special color names are not respected but instead processed according to the rules. Hence, "red" will result in #000E0D and not #FF0000

Test cases

Note that the # is completely optional

red                              -> #000E0D
rules                            -> #000E00
1234567890ABCDE1234567890ABCDE   -> #34CD89
FLUFF                            -> #F00FF0
F                                -> #0F0000
-> #000000
zqbttv                           -> #00B000
6db6ec49efd278cd0bc92d1e5e072d68 -> #6ECDE0
102300450067                     -> #100000
GHIJKL                           -> #000000
000000                           -> #000000


EDIT

These are the longest Jelly and Vyxal answers I've seen on this site up to this point :o

EDIT 2

Lecdi pointed out that the specification had a flaw that caused undefined behaviour if the input turned into 000000 at some point, such as GHIJKL. As of now, all answers seem to handle this with the exception of lyxal's 64 byte Vyxal answer.

The flaw in the spec should now be fixed. Sorry!

• A perfect mix of sadism and creativity. Jul 31 at 13:54
• @JonathanAllan About the mixed cases, that was a inconsistency I made, it's fixed now. Casing is irrelevant as described by the 4th rule's very last point. I also added your suggested test case, as I concur that the correct solution should be #100000. Jul 31 at 19:15
• Are JavaScript answers allowed to require IE as an execution requirement? :P Aug 1 at 0:38
• Related Q&A on Stack Overflow: Why does HTML think “chucknorris” is a color? Aug 1 at 0:39
• @Bergi thought of that too ;-) and I made a 44 solution that I didn't test on IE v=>((s=document.body.style).color=v,s.color), but the last rule "special color names are not respected" would make it fail anyway... Aug 1 at 1:57

⇧ƛk^$c∧;ṅ3/\0ÞḞṅ3/8Nvȯ∩\03*:£øl:L2∵Ẏ¥p¥p2Nȯ∩  Try it Online! A huge mess, but at least I outgolfed lyxal. • You outgolfed lyxal, but you forgot about me evil laugh Aug 3 at 2:05 JavaScript + HTML, 50 bytes document.body.setAttribute('bgcolor','#'+prompt())  Works in any browser, as the legacy behavior was kept with bgcolor (even though it won't work with CSS). Displays as graphical output. <script>document.body.setAttribute("bgcolor",'#'+prompt());</script> If graphical output is not allowed, then: JavaScript + HTML, 97 bytes c=document.body;c.setAttribute('bgcolor','#'+prompt());alert(getComputedStyle(c).backgroundColor)  <script>c=document.body;c.setAttribute('bgcolor','#'+prompt());alert(getComputedStyle(c).backgroundColor)</script> Uses the computed styles generated. Will output like rgb(X, Y, Z). To output a hex code, this would be 156 bytes: c=document.body;c.setAttribute('bgcolor','#'+prompt());alert(getComputedStyle(c).backgroundColor.match(/\d+/g).map(x=>(x|256).toString(16).slice(1)).join)  <script>c=document.body;c.setAttribute('bgcolor','#'+prompt());alert(getComputedStyle(c).backgroundColor.match(/\d+/g).map(x=>(x|256).toString(16).slice(1)).join)</script> Jelly, 44 39 bytes -5 thanks to UnrelatedString (clever use of ;Ṫ for the empty string special case). ØhḊiⱮŒla;Ṫ$œs3z0ZŻ€ṫ€-7ZḊẸ€Ḣ¬Ɗ¡ƬḊƇṪḣ2ZF


A full program that accepts a string and prints the resulting colour code (keeping character casing from the input*).

Try it online! Or see the test-suite.

How?

ØhḊiⱮŒla;Ṫ$œs3z0ZŻ€ṫ€-7ZḊẸ€Ḣ¬Ɗ¡ƬḊƇṪḣ2ZF - ...f(X) Øh - hex characters = "0123456789abcdef" Ḋ - dequeue -> "123456789abcdef" Œl - lower-case X Ɱ - map with: i - first 1-indexed index or 0 if not found$                            - last two links as a monad - f(X):
Ṫ                             -   tail (yields zero when X is empty)
;                              -   X (without its tail) concatenated with that (its tail or a zero)
a                               - logical AND the hex-char-indicator list with X or [0] (vectorises)
œs3                         - split into three equal chunks
Ż€                    - prefix each with a zero
ṫ€-7                - tail each from index -7 -> last (up to) eight values
Z               - transpose
Ƭ        - collect up while distinct, applying:
¡         -   repeat...
Ẹ€             -     any? for each
¬           -     logical NOT
Ḋ               -   ...action: dequeue
Ƈ      - keep those which are truthy under:
Ḋ       -   dequeue
Ṫ     - tail
ḣ2   - head to index two
Z  - transpose
F - flatten
- implicit, smashing print


JavaScript (Browser), 138 bytes, non-competitive

c=>(document.body.innerHTML=<font id=g color=${c}>,getComputedStyle(g)).color.match(/\d+/g).map(n=>(n|256).toString(16).slice(1)).join  The same color parsing algorithm also works on browsers other than IE (I had tested on my Firefox 103 and it works). But the code will output #ff0000 for input red so marked non-competitive. f= c=>(document.body.innerHTML=<font id=g color=${c}>,getComputedStyle(g)).color.match(/\d+/g).map(n=>(n|256).toString(16).slice(1)).join

console.log(f('red                             '));
console.log(f('rules                           '));
console.log(f('1234567890ABCDE1234567890ABCDE  '));
console.log(f('FLUFF                           '));
console.log(f('F                               '));
console.log(f('                                '));
console.log(f('zqbttv                          '));
console.log(f('6db6ec49efd278cd0bc92d1e5e072d68'));
console.log(f('102300450067                    '));

Save ~30 bytes by Kaiido

• 149: f=c=>eval(((document.body.innerHTML=<font id=g color=${c}>),getComputedStyle(g).color));rgb=(...a)=>a.map(n=>(n+256).toString(16).slice(1)).join Aug 1 at 12:54 • Shouldn't this be 'HTML+Javascript', since you need a DOM environment for execution? Aug 1 at 14:43 • You can do this in HTML in 74 bytes, output letter X in the actual color: <font>X<input oninput=this.parentElement.setAttribute('color',this.value) Aug 1 at 15:18 • @ccprog the goal is to output an hex string, not to set an element's color. (And you could outgolf your HTML: onevent attributes use the target as context, so this is never needed there, but setting an id on the parent makes it available as a global, so you could shave 17 bytes if I count correctly <font id=f>X<input oninput=f.setAttribute('color',value) (untested, I'm on a phone, but that doesn't answer the question anyway) (Oh and since you don't read it I don't think you need setAttribute, .color should work, if I'm not mistaken). Aug 1 at 15:43 • @Kaiido quote: "Output any representation of the resulting RGB color (including but not limited to the actual color". Otherwise, you are right: <font id=f>X<input oninput=f.color=value works just fine with 41 bytes. Aug 1 at 16:04 JavaScript, 238 234 bytes Way longer than the others, but actually competing! c=>(M=Math,s='slice',t=M.ceil(c.length/3),c=c.padEnd(M.max(t*3,3)).replace(/[g-zG-Z ]/g,'0'),c=[c[s](0,t),c[s](t,t*2),c[s](t*2)]).map(p=>p[s](i=M.min(...c.map(p=>(r=p.search(/[^0].{0,7}$/),r<0?3e333:r))),i+2).padStart(2,'0')).join('')


Less golfed:

c => (
M = Math, s = 'slice', // these are shorter
t = M.ceil(c.length / 3), // prepare length for the parts
c = c
.padEnd(M.max(t*3, 3)) // pad with spaces until length divisible by 3, with a minimum of 3
.replace(/[g-zG-Z ]/g, '0'), // replace all non-hex characters with '0'
c = [c[s](0, t), c[s](t, t*2), c[s](t*2)] // split into 3 equal parts
).map(p => (
i = M.min(...c.map(p=>(r=p.search(/[^0].{0,7}$/),r<0?3e333:r))), // search for a character that's not '0' in the last 8 characters, returning its index, or infinity when negative (no match found), and take the lowest of the parts p[s](i, i+2).padStart(2, '0') // slice max two characters from the lowest non-'0', and pad to two characters )).join('')  Vyxal, 67 64 bytes k6:⇧∪ṅ\^pøB?0øṙ₅:ǒ3εǒ+↲›3/?L4<[2↳›|8Nvȯ{:vh0J≈|ƛh0=ßḢ}ƛ₃[0p|2Ẏ}ṅ  Try it Online! A big mess of unicode and lesser-used overloads of things Explained There are 10 parts here: Generating the hex-digit regex: k6:⇧∪ṅ\^pøB Replacing non-hex with 0: ?0øṙ Padding to nearest multiple of 3: ₅:ǒ3εǒ+↲› Splitting into 3 equal parts: 3/ The length 4 conditional branch: ?L4<[ Making each bit 2 digits with 0s if needed: 2↳› | Getting the last 8 characters: 8Nvȯ Removing 0s until something doesn't start with a 0: {:vh0J≈|ƛh0=ßḢ} Either prepending a 0 or getting the last 2 chars: ƛ₃[0p|2Ẏ} Outputting the result: ṅ  Generating the hex-digit regex k6:⇧∪ṅ\^pøB k6 # The string "0123456789abcdef" :⇧ # Uppercased ("0123456789ABCDEF") ∪ṅ # Unioned with the lowercase string ("0123456789abcdefABCDEF") \^p # With a caret prepended ("^0123456789abcdefABCDEF") øB # Surrounded in "[]" ("[^0123456789abcdefABCDEF]")  This regex ([^0123456789abcdefABCDEF], called re from here) will be used in the next step to determine which characters to replace with 0s. Replacing non-hex with 0 ?0øṙ # Before this part, the stack is [re] ? # Push the input : [re, input] 0 # Push a 0 to the stack : [re, input, 0] øṙ # Replace matches of re in input with 0  This completes the step of replacing invalid characters with 0s, which is common to strings less than 4 chars and strings with at least 4 chars. This string will be called 0-str from here. Padding to nearest multiple of 3 ₅:ǒ3εǒ+↲› # Before this part, the stack is [0-str] ₅: # Push two copies of the length of 0-str without popping it ǒ3ε # Modulo the first copy by 3 and take the absolute difference of that and 3 to get how far away the length is from the next multiple of 3. Note that this may result in the result being 3 because a string of length that is divisible by 3 will return 3, as 3 - (length % 3 => 0) = 3 ǒ # Modulo 3 again to make a 3 a 0 +↲› # Add that to the remaining copy of the length to get how many characters should be in the padded string, left pad 0-str with spaces to that many characters and replace all spaces with 0s.  The result of this step is common to both string length possibilities and will be referred to as pad-0-str. Splitting into 3 equal parts 3/ # Before this part, the stack is [pad-0-str] 3/ # Divide the string into 3 equal parts  Yes, there really is a built-in for this. The result will be referred to as 3-part-pad. The length 4 conditional branch ?L4<[...|...} # Before this part, the stack is [3-pad-str] ?L # Is the length of the input 4< # Less than 4? [... # If so, move onto the steps for a string with less than 4 characters |...} # Otherwise, move onto the steps for a string with at least 4 characters. A } is used instead of a ] because it saves a byte over ;] later on.  The stack hasn't changed after this step. Less than 4 - Making each bit 2 digits with 0s if needed 2↳› # Before this part, the stack is [3-pad-str] 2↳ # Right pad each part of 3-pad-str to be two characters, using spaces. This is needed because components of 3-pad-str will be empty if the input is the empty string. In the case of the empty string, this returns " ". Otherwise, it returns " " + char › # Replace the spaces with 0s. This turns " " into "00" and everything else into "0" + char.  The result of this will be called res. At least 4 - Getting the last 8 characters 8Nvȯ # Before this part, the stack is [3-pad-str] 8N # Push -8 to the stack : [3-pad-str, -8] vȯ # Push [part[-8:] for part in 3-pad-str]  The result of this will be referred to as last-8 At least 4 - Removing 0s until something doesn't start with a 0 {:vh0J≈|ƛh0=ßḢ} # Before this part, the stack is [last-8] { # While ... :vh # the first character of each part of last-8 (leaving last-8 on the stack)... 0J≈ # are all equal to 0: |ƛh0= # For each item, check if the first character is 0 ßḢ} # And if so, remove the head of that part. The } here closes both the map lambda and the while loop.  This is probably the messiest section. The result of this will be referred to as second-last. At least 4 - Either prepending a 0 or getting the last 2 chars ƛ₃[0p|2Ẏ} # Before this part, the stack is [second-last] ƛ # For each item in second-last, which has leading 0s removed: ₃[ # If the length is 1: 0p # Prepend a 0 |2Ẏ} # Else: take the last two characters. # The } here closes both this inner if-statement, the map lambda, and the outer if-statement  The result of this part will be referred to as res. This is analogous to the step taken in the less than 4 branch. Outputting the result ṅ # Join res on empty string - concatenate into a single string.  I could have used the s flag here, but at this point, we're already play the long golf game, so I thought I might as well go flagless. • Would a map not be shorter than a regex? I’m on a phone rn so I can’t test Jul 31 at 15:43 • Get outgolfed lol Jul 31 at 19:09 05AB1E, 48 47 bytes lA6.$S0:₄¦«3ô¨S3ä8δ.£'e¶:øJ0Û¶'e:2£₄¦D‚ì2.£€Sø˜


Output as a lowercase list of characters without #.

Explanation:

Step 1: Convert the input to lowercase, and replace all invalid letters with a 0:

l              # Lowercase the (implicit) input-string
A             # Push the lowercase alphabet
6.$# Remove its first 6 characters: "ghijklmnopqrstuvwxyz" S # Convert it to a list of characters 0: # And replace all those characters with a "0" in the uppercase input  Try just step 1 online. Step 2: Pad with trailing 0s, and split it into three equal-sized parts (with each part as a list of characters): ₄ # Push 1000 ¦ # Remove it's first character: "000" « # Append it 3ô # Split it into parts of size 3 ¨ # Remove the last (potentially shorter) part S # Convert it to a flattened list of characters 3ä # Split it into 3 equal-sized parts  Try just the first two steps online. Step 3: Only keep the last 8 characters of each inner list:  δ # Map over each inner list 8 .£ # Only keep (up to) the last 8 characters  Try just the first three steps online. Step 4: Remove leading 0s from each list, until a column doesn't start with a 0 anymore: 'e¶: '# Replace all "e" with a newline # (workaround, because "0e0" would be interpret as 0 in 05AB1E) ø # Zip/transpose; swapping rows/columns J # Join each inner list together 0Û # Trim all leading 0s ¶'e: '# Undo the workaround, by replacing all newlines back to "e"s  Try just the first four steps online. Step 5: Only keep the first two characters of each row, potentially prepending the result with 0s, and output the final result: 2£ # Only keep the first two triplets of the list ₄¦ # Push "000" again D‚ # Pair it with itself: ["000","000"] ì # Prepend this in front of the list 2.£ # Now only keep the last two triplets €S # Convert each inner string back to a list of characters ø # Zip/transpose back; swapping rows/columns ˜ # Flatten the pair of triplets of characters # (after which the sixtet of characters is output implicitly as result)  Rust, 372367 340 bytes |mut s:Vec<u8>|{for c in &mut s{if!c.is_ascii_hexdigit(){*c=48}}s.extend(b"000");let d:Vec<_>=s.chunks((s.len()-1).max(3)/3).take(3).map(|c|&c[c.len().saturating_sub(8)..]).collect();d.iter().flat_map(|c|match&c[d.iter().map(|c|c.iter().take_while(|c|**c==48).count()).min().unwrap()..]{[]=>*b"00",&[c]=>[48,c],&[x,y,..]=>[x,y]}).collect()}  Ungolfed: fn f(mut s: Vec<u8>) -> Vec<u8> { for c in &mut s { if !c.is_ascii_hexdigit() { *c = b'0' } } s.extend(b"000"); let chunks: Vec<_> = s .chunks((s.len() - 1).max(3) / 3) .take(3) .map(|c| &c[c.len().saturating_sub(8)..]) .collect(); let to_remove = chunks .iter() .map(|c| c.iter().take_while(|c| **c == b'0').count()) .min() .unwrap(); chunks .iter() .flat_map(|c| match &c[to_remove..] { [] => *b"00", &[c] => [b'0', c], &[c1, c2, ..] => [c1, c2], }) .collect() }  Playground The chunking is a little tricky, but otherwise a relatively boring solution. • Why allow the Clippy lint? c.is_ascii_hexdigit() is shorter than (*c as char).is_digit(16). Aug 2 at 10:47 • Because I totally missed that that method also exists for u8! I silenced the lint in an earlier iteration where c was a char, so c.is_digit(16) actually was shorter than c.is_ascii_hexdigit(), and then didn't pay attention when refactoring. Thanks! Aug 2 at 12:30 Vyxal, 29 bytes S⇩3ẇ3↲∑k6Ǐ~vḟİ∑3/⟑8NȯHH2∆Z2Ẏ₴  Try it Online! S⇩ # Stringify ("" -> 0 edgecase) and lowercase 3ẇ # Cut into chunks of length 3 3↲ # Pad each to length 3 with spaces (will be zeroed) ∑ # Concatenate k6 # Lowercase hex Ǐ # Append the leading zero ~vḟ # Without popping, for each char in input, find it in the hex İ # Index into the hex + 0 - ones not found, -1s, get indexed into the end ∑ # Concat into a string 3/ # Divide into 3 parts ⟑ # Over each... 8Nȯ # Get last 8 chars HH # Convert to/from hex to remove leading zeroes 2∆Z # zfill each to length 2 2Ẏ # Get the first two chars of each ₴ # Print  • @Steffan Fixed both. Aug 3 at 2:17 • This still doesn't work for an empty string, it's just that Vyxal treats an empty input as 0 Aug 3 at 2:18 • @Steffan It works when empty input is given, so it’s fine Aug 3 at 3:40 • It's not empty input, it's no input. Aug 3 at 3:41 • @Steffan passing the correct strings in with a consistent I/O format (plain, without quotes) is perfectly valid. Aug 3 at 3:44 Retina 0.8.2, 117 bytes Topa-fA-FRd$
00
^((.)*?)((?<_-2>.)*)((?<-_>.)*)0?0?$00$1¶00$3¶00$4¶
.+(.{8})
$1 +^0(..+¶)0(.+¶)0$1$2 (..).*¶$1


Try it online! Link includes test cases. Explanation:

Topa-fA-FRd


Replace non-hex digits with zeros.

$00  Append two zeros in case the length is not a multiple of 3. ^((.)*?)((?<_-2>.)*)((?<-_>.)*)0?0?$
00$1¶00$3¶00$4¶  Divide the string into three equal parts, discarding the trailing zeros just added if necessary. Prefix 00 to each part in case it is short. .+(.{8})$1


Trim each part to the last 8 digits.

+^0(..+¶)0(.+¶)0
$1$2


Trim leading zeros while all parts have one, but don't trim to fewer than two digits.

(..).*¶
$1  Keep only the first two digits of each part and join everything together. C#, 235229 227 bytes -6 bytes thanks to Kevin Cruijssen s=>{int l;for(s=Regex.Replace(s,"[^0-9a-fA-F]|^$","0");(l=s.Length)%3>0;s+="0");for(s=l<4?"0"+s[0]+0+s[1]+0+s[2]:s;(l=s.Length/3)>8|s[0]+s[l]+s[2*l]<145&l>2;s=string.Concat(s.Where((_,i)=>i%l>0)));return s.Where((_,i)=>i%l<2);}


Try it online!

Ungolfed:

input => {
//the three while loops are golfed into for loops in the submission, with the last two then being combined into one

// replace non hex chars with 0
input = Regex.Replace(input, "[^0-9a-fA-F]|^\$", "0");

// pad input to next multiple of 3
while (input.Length % 3 > 0) {
input += "0";
}
// add 0 at the start of each section if the input is now 3 chars long
// the final two zeroes here get coerced to strings
if (input.Length == 3) input = "0" + input[0] + 0 + input[1] + 0 + input[2];

// used to keep track of the current section length (changes as 0s are removed)
int partLength = input.Length / 3;

// if part length is >8 continuously remove the first char from each part (notice the first char of each part is always 0 mod partLength)
while (partLength > 8) {
// because LINQ, the .Where has to be cast to something to actually evaluate within a while, string.Concat is the shortest way (including other changes to have other variables be the correct type)
input = string.Concat(input.Where((_, i) => i % partLength > 0));
}

//if every part starts with 0, remove the first char from each
//this is golfed into a sum using char codes in the submission ((int)'0' = 48)
while (input[0] == '0' && input[partLength] == '0' && input[2 * partLength] == '0' && partLength > 2) {
input = string.Concat(input.Where((_, i) => i % partLength > 0));
}
//return the first two chars from each part
return input.Where((_, i) => i % partLength < 2);
}

• Nice answer! :) You can save a few bytes: -1 byte by adding a variable for s.Length: int l; to int L,l; + s.Length%3>0 to (L=s.Length)%3>0 + s.Length<4? to L<4?. Then -4 bytes by changing "0"+s[0]+"0"+s[1]+"0"+s[2] to "0"+s[0]+0+s[1]+0+s[2]. And you don't have to count the trailing ; of the lambda, so it can move to the footer for another -1. Try it online - 229 bytes Aug 8 at 14:41
• It turns out I can actually just use l for s.length instead of a new variable L and save another 2 bytes.
– Acer
Aug 11 at 9:07