I found Spektre's answer useful, in that many people won't be in a position to apply the rigorous CIE-based methodology from other answers, but would still like a ready-to-run solution with some basis in physical reality.
To that end, I made a revised algorithm by fitting Spektre's data with a B-spline of degree 2 using wavelength as the parameter. This has the advantage that the RGB color varies smoothly with wavelength (it has a continuous first derivative), and is a bit simpler since most of the calculation has been done in advance. This form is also amenable to vector (SIMD) processing, where that is relevant.
In Javascript:
function wavelengthToRGB (λ) {
const C=[
350,
3.08919e-5,-2.16243e-2, 3.78425e+0,
0.00000e+0, 0.00000e+0, 0.00000e+0,
4.33926e-5,-3.03748e-2, 5.31559e+0,
397,
-5.53952e-5, 4.68877e-2,-9.81537e+0,
6.13203e-5,-4.86883e-2, 9.66463e+0,
4.41410e-4,-3.46401e-1, 6.80468e+1,
423,
-3.09111e-5, 2.61741e-2,-5.43445e+0,
1.85633e-4,-1.53857e-1, 3.19077e+1,
-4.58520e-4, 4.14940e-1,-9.29768e+1,
464,
2.86786e-5,-2.91252e-2, 7.39499e+0,
-1.66581e-4, 1.72997e-1,-4.39224e+1,
4.37994e-7,-1.09728e-2, 5.83495e+0,
514,
2.06226e-4,-2.11644e-1, 5.43024e+1,
-6.65652e-5, 7.01815e-2,-1.74987e+1,
9.41471e-5,-1.07306e-1, 3.05925e+1,
565,
-2.78514e-4, 3.36113e-1,-1.00439e+2,
-1.79851e-4, 1.98194e-1,-5.36623e+1,
1.12142e-5,-1.35916e-2, 4.11826e+0,
606,
-1.44403e-4, 1.73570e-1,-5.11884e+1,
2.47312e-4,-3.19527e-1, 1.03207e+2,
0.00000e+0, 0.00000e+0, 0.00000e+0,
646,
6.24947e-5,-9.37420e-2, 3.51532e+1,
0.00000e+0, 0.00000e+0, 0.00000e+0,
0.00000e+0, 0.00000e+0, 0.00000e+0,
750
];
let [r,g,b] = [0,0,0];
if (λ >= C[0] && λ < C[C.length-1]) {
for (let i=0; i<C.length; i+=10) {
if (λ < C[i+10]) {
const λ2 = λ*λ;
r = C[i+1]*λ2 + C[i+2]*λ + C[i+3];
g = C[i+4]*λ2 + C[i+5]*λ + C[i+6];
b = C[i+7]*λ2 + C[i+8]*λ + C[i+9];
break;
}
}
}
return [r,g,b];
}
The array in this function contains the bounding wavelengths for each span (in nm), and between each boundary there are three sets of λ², λ¹ and λ⁰
coefficients – one each for red, green and blue.
If you want to use different units, you can convert the boundary values accordingly (but reverse the search order if you are using reciprocal units, e.g. THz, eV or cm-1).
You can also premultiply all the coefficients by 255 (and cast to int) if you want to generate 8-bit color components directly.