1 // Written in the D programming language.
2 
3 /**
4 This module implements the $(LINK2 https://en.wikipedia.org/wiki/RGB_color_space, RGB) _color type.
5 
6 RGB is the most common expression of colors used in computing, where a _color is specified as some
7 amount of red, green and blue primaries.
8 
9 RGB is highly parametric, and comes in many shapes and sizes, with the most common being
10 $(LINK2 https://en.wikipedia.org/wiki/SRGB, sRGB), which is conventionally used on
11 computer monitors, and standard for use on the web.
12 
13 RGB colors require the RGB _color space parameters to be defined to be considered 'absolute' colors.
14 
15 Authors:    Manu Evans
16 Copyright:  Copyright (c) 2015, Manu Evans.
17 License:    $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0)
18 Source:     $(PHOBOSSRC std/experimental/color/_rgb.d)
19 */
20 module std.experimental.color.rgb;
21 
22 import std.experimental.color;
23 import std.experimental.color.colorspace;
24 import std.experimental.color.xyz : XYZ, isXYZ;
25 import std.experimental.normint;
26 
27 import std.traits : isInstanceOf, isNumeric, isIntegral, isFloatingPoint, isSomeChar, Unqual;
28 import std.typetuple : TypeTuple;
29 import std.typecons : tuple;
30 
31 @safe pure nothrow @nogc:
32 
33 
34 /**
35 Detect whether $(D_INLINECODE T) is an RGB color.
36 */
37 enum isRGB(T) = isInstanceOf!(RGB, T);
38 
39 ///
40 unittest
41 {
42     static assert(isRGB!(RGB!("bgr", ushort)) == true);
43     static assert(isRGB!LA8 == true);
44     static assert(isRGB!int == false);
45 }
46 
47 
48 // DEBATE: which should it be?
49 template defaultAlpha(T)
50 {
51 /+
52     enum defaultAlpha = isFloatingPoint!T ? T(1) : T.max;
53 +/
54     enum defaultAlpha = T(0);
55 }
56 
57 
58 /**
59 An RGB color, parameterised with components, component type, and color space specification.
60 
61 Params: components_ = Components that shall be available. Struct is populated with components in the order specified.$(BR)
62                       Valid components are:$(BR)
63                         "r" = red$(BR)
64                         "g" = green$(BR)
65                         "b" = blue$(BR)
66                         "a" = alpha$(BR)
67                         "l" = luminance$(BR)
68                         "x" = placeholder/padding (no significant value)
69         ComponentType_ = Type for the color channels. May be a basic integer or floating point type.
70         linear_ = Color is stored with linear luminance.
71         colorSpace_ = Color will be within the specified color space.
72 */
73 struct RGB(string components_, ComponentType_, bool linear_ = false, RGBColorSpace colorSpace_ = RGBColorSpace.sRGB)
74     if (isNumeric!ComponentType_)
75 {
76 @safe pure:
77 
78     /** Construct a color from a string. */
79     this(C)(const(C)[] str) if (isSomeChar!C)
80     {
81         this = colorFromString!(typeof(this))(str);
82     }
83     ///
84     unittest
85     {
86         static assert(RGB8("#8000FF")  == RGB8(0x80,0x00,0xFF));
87         static assert(RGBA8("#908000FF") == RGBA8(0x80,0x00,0xFF,0x90));
88     }
89 
90 nothrow @nogc:
91 
92     // RGB colors may only contain components 'rgb', or 'l' (luminance)
93     // They may also optionally contain an 'a' (alpha) component, and 'x' (unused) components
94     static assert(allIn!("rgblax", components), "Invalid Color component '"d ~ notIn!("rgblax", components) ~ "'. RGB colors may only contain components: r, g, b, l, a, x"d);
95     static assert(anyIn!("rgbal", components), "RGB colors must contain at least one component of r, g, b, l, a.");
96     static assert(!canFind!(components, 'l') || !anyIn!("rgb", components), "RGB colors may not contain rgb AND luminance components together.");
97 
98     static if (isFloatingPoint!ComponentType_)
99     {
100         /** Type of the color components. */
101         alias ComponentType = ComponentType_;
102     }
103     else
104     {
105         /** Type of the color components. */
106         alias ComponentType = NormalizedInt!ComponentType_;
107     }
108 
109     /** The color components that were specified. */
110     enum string components = components_;
111     /** The colors color space. */
112     enum RGBColorSpace colorSpace = colorSpace_;
113     /** The color space descriptor. */
114     enum RGBColorSpaceDesc!F colorSpaceDesc(F = double) = rgbColorSpaceDef!F(colorSpace_);
115     /** If the color is stored linearly (without gamma applied). */
116     enum bool linear = linear_;
117 
118 
119     // mixin will emit members for components
120     template Components(string components)
121     {
122         static if (components.length == 0)
123             enum Components = "";
124         else
125             enum Components = ComponentType.stringof ~ ' ' ~ components[0] ~ " = 0;\n" ~ Components!(components[1..$]);
126     }
127     mixin(Components!components);
128 
129     /** Test if a particular component is present. */
130     enum bool hasComponent(char c) = mixin("is(typeof(this."~c~"))");
131     /** If the color has alpha. */
132     enum bool hasAlpha = hasComponent!'a';
133 
134 
135     /** Return the RGB tristimulus values as a tuple.
136         These will always be ordered (R, G, B).
137         Any color channels not present will be 0. */
138     @property auto tristimulus() const
139     {
140         static if (hasComponent!'l')
141         {
142             return tuple(l, l, l);
143         }
144         else
145         {
146             static if (!hasComponent!'r')
147                 enum r = ComponentType(0);
148             static if (!hasComponent!'g')
149                 enum g = ComponentType(0);
150             static if (!hasComponent!'b')
151                 enum b = ComponentType(0);
152             return tuple(r, g, b);
153         }
154     }
155     ///
156     unittest
157     {
158         // tristimulus returns tuple of R, G, B
159         static assert(BGR8(255, 128, 10).tristimulus == tuple(NormalizedInt!ubyte(255), NormalizedInt!ubyte(128), NormalizedInt!ubyte(10)));
160     }
161 
162     /** Return the RGB tristimulus values + alpha as a tuple.
163         These will always be ordered (R, G, B, A). */
164     @property auto tristimulusWithAlpha() const
165     {
166         static if (!hasAlpha)
167             enum a = defaultAlpha!ComponentType;
168         return tuple(tristimulus.expand, a);
169     }
170     ///
171     unittest
172     {
173         // tristimulusWithAlpha returns tuple of R, G, B, A
174         static assert(BGRA8(255, 128, 10, 80).tristimulusWithAlpha == tuple(NormalizedInt!ubyte(255), NormalizedInt!ubyte(128), NormalizedInt!ubyte(10), NormalizedInt!ubyte(80)));
175     }
176 
177     /** Construct a color from RGB and optional alpha values. */
178     this(ComponentType r, ComponentType g, ComponentType b, ComponentType a = defaultAlpha!ComponentType)
179     {
180         foreach (c; TypeTuple!("r","g","b","a"))
181             mixin(ComponentExpression!("this._ = _;", c, null));
182         static if (canFind!(components, 'l'))
183             this.l = toGrayscale!(linear, colorSpace)(r, g, b); // ** Contentious? I this this is most useful
184     }
185 
186     /** Construct a color from a luminance and optional alpha value. */
187     this(ComponentType l, ComponentType a = defaultAlpha!ComponentType)
188     {
189         foreach (c; TypeTuple!("l","r","g","b"))
190             mixin(ComponentExpression!("this._ = l;", c, null));
191         static if (canFind!(components, 'a'))
192             this.a = a;
193     }
194 
195     static if (!isFloatingPoint!ComponentType_)
196     {
197         /** Construct a color from RGB and optional alpha values. */
198         this(ComponentType.IntType r, ComponentType.IntType g, ComponentType.IntType b, ComponentType.IntType a = defaultAlpha!(ComponentType.IntType))
199         {
200             foreach (c; TypeTuple!("r","g","b","a"))
201                 mixin(ComponentExpression!("this._ = ComponentType(_);", c, null));
202             static if (canFind!(components, 'l'))
203                 this.l = toGrayscale!(linear, colorSpace)(ComponentType(r), ComponentType(g), ComponentType(b)); // ** Contentious? I this this is most useful
204         }
205 
206         /** Construct a color from a luminance and optional alpha value. */
207         this(ComponentType.IntType l, ComponentType.IntType a = defaultAlpha!(ComponentType.IntType))
208         {
209             foreach (c; TypeTuple!("l","r","g","b"))
210                 mixin(ComponentExpression!("this._ = ComponentType(l);", c, null));
211             static if (canFind!(components, 'a'))
212                 this.a = ComponentType(a);
213         }
214     }
215 
216     /**
217     Cast to other color types.
218 
219     This cast is a convenience which simply forwards the call to convertColor.
220     */
221     Color opCast(Color)() const if (isColor!Color)
222     {
223         return convertColor!Color(this);
224     }
225 
226     // comparison
227     bool opEquals(typeof(this) rh) const
228     {
229         // this is required to exclude 'x' components from equality comparisons
230         return tristimulusWithAlpha == rh.tristimulusWithAlpha;
231     }
232 
233     /** Unary operators. */
234     typeof(this) opUnary(string op)() const if (op == "+" || op == "-" || (op == "~" && is(ComponentType == NormalizedInt!U, U)))
235     {
236         Unqual!(typeof(this)) res = this;
237         foreach (c; AllComponents)
238             mixin(ComponentExpression!("res._ = #_;", c, op));
239         return res;
240     }
241     ///
242     unittest
243     {
244         static assert(+UVW8(1,2,3) == UVW8(1,2,3));
245         static assert(-UVW8(1,2,3) == UVW8(-1,-2,-3));
246 
247         static assert(~RGB8(1,2,3) == RGB8(0xFE,0xFD,0xFC));
248         static assert(~UVW8(1,2,3) == UVW8(~1,~2,~3));
249     }
250 
251     /** Binary operators. */
252     typeof(this) opBinary(string op)(typeof(this) rh) const if (op == "+" || op == "-" || op == "*")
253     {
254         Unqual!(typeof(this)) res = this;
255         foreach (c; AllComponents)
256             mixin(ComponentExpression!("res._ #= rh._;", c, op));
257         return res;
258     }
259     ///
260     unittest
261     {
262         static assert(RGB8(10,20,30)       + RGB8(4,5,6) == RGB8(14,25,36));
263         static assert(UVW8(10,20,30)       + UVW8(4,5,6) == UVW8(14,25,36));
264         static assert(RGBAf32(10,20,30,40) + RGBAf32(4,5,6,7) == RGBAf32(14,25,36,47));
265 
266         static assert(RGB8(10,20,30)       - RGB8(4,5,6) == RGB8(6,15,24));
267         static assert(UVW8(10,20,30)       - UVW8(4,5,6) == UVW8(6,15,24));
268         static assert(RGBAf32(10,20,30,40) - RGBAf32(4,5,6,7) == RGBAf32(6,15,24,33));
269 
270         static assert(RGB8(10,20,30)       * RGB8(128,128,128) == RGB8(5,10,15));
271         static assert(UVW8(10,20,30)       * UVW8(-64,-64,-64) == UVW8(-5,-10,-15));
272         static assert(RGBAf32(10,20,30,40) * RGBAf32(0,1,2,3) == RGBAf32(0,20,60,120));
273     }
274 
275     /** Binary operators. */
276     typeof(this) opBinary(string op, S)(S rh) const if (isColorScalarType!S && (op == "*" || op == "/" || op == "%" || op == "^^"))
277     {
278         Unqual!(typeof(this)) res = this;
279         foreach (c; AllComponents)
280             mixin(ComponentExpression!("res._ #= rh;", c, op));
281         return res;
282     }
283     ///
284     unittest
285     {
286         static assert(RGB8(10,20,30)       * 2 == RGB8(20,40,60));
287         static assert(UVW8(10,20,30)       * 2 == UVW8(20,40,60));
288         static assert(RGBAf32(10,20,30,40) * 2 == RGBAf32(20,40,60,80));
289 
290         static assert(RGB8(10,20,30)       / 2 == RGB8(5,10,15));
291         static assert(UVW8(-10,-20,-30)    / 2 == UVW8(-5,-10,-15));
292         static assert(RGBAf32(10,20,30,40) / 2 == RGBAf32(5,10,15,20));
293 
294         static assert(RGB8(10,20,30)       * 2.0 == RGB8(20,40,60));
295         static assert(UVW8(10,20,30)       * 2.0 == UVW8(20,40,60));
296         static assert(RGBAf32(10,20,30,40) * 2.0 == RGBAf32(20,40,60,80));
297         static assert(RGB8(10,20,30)       * 0.5 == RGB8(5,10,15));
298         static assert(UVW8(-10,-20,-30)    * 0.5 == UVW8(-5,-10,-15));
299         static assert(RGBAf32(5,10,15,20)  * 0.5 == RGBAf32(2.5,5,7.5,10));
300 
301         static assert(RGB8(10,20,30)       / 2.0 == RGB8(5,10,15));
302         static assert(UVW8(-10,-20,-30)    / 2.0 == UVW8(-5,-10,-15));
303         static assert(RGBAf32(10,20,30,40) / 2.0 == RGBAf32(5,10,15,20));
304         static assert(RGB8(10,20,30)       / 0.5 == RGB8(20,40,60));
305         static assert(UVW8(10,20,30)       / 0.5 == UVW8(20,40,60));
306         static assert(RGBAf32(10,20,30,40) / 0.5 == RGBAf32(20,40,60,80));
307     }
308 
309     /** Binary assignment operators. */
310     ref typeof(this) opOpAssign(string op)(typeof(this) rh) if (op == "+" || op == "-" || op == "*")
311     {
312         foreach (c; AllComponents)
313             mixin(ComponentExpression!("_ #= rh._;", c, op));
314         return this;
315     }
316 
317     /** Binary assignment operators. */
318     ref typeof(this) opOpAssign(string op, S)(S rh) if (isColorScalarType!S && (op == "*" || op == "/" || op == "%" || op == "^^"))
319     {
320         foreach (c; AllComponents)
321             mixin(ComponentExpression!("_ #= rh;", c, op));
322         return this;
323     }
324 
325 package:
326 
327     alias ParentColor = XYZ!(FloatTypeFor!ComponentType);
328 
329     static To convertColorImpl(To, From)(From color) if (isRGB!From && isRGB!To)
330     {
331         alias ToType = To.ComponentType;
332         alias FromType = From.ComponentType;
333 
334         auto src = color.tristimulusWithAlpha;
335 
336         static if (From.colorSpace == To.colorSpace && From.linear == To.linear)
337         {
338             // color space is the same, just do type conversion
339             return To(cast(ToType)src[0], cast(ToType)src[1], cast(ToType)src[2], cast(ToType)src[3]);
340         }
341         else
342         {
343             // unpack the working values
344             alias WorkType = WorkingType!(FromType, ToType);
345             WorkType r = cast(WorkType)src[0];
346             WorkType g = cast(WorkType)src[1];
347             WorkType b = cast(WorkType)src[2];
348 
349             static if (From.linear == false)
350             {
351                 r = toLinear!(From.colorSpace)(r);
352                 g = toLinear!(From.colorSpace)(g);
353                 b = toLinear!(From.colorSpace)(b);
354             }
355             static if (From.colorSpace != To.colorSpace)
356             {
357                 enum toXYZ = rgbToXyzMatrix(From.colorSpaceDesc!WorkType);
358                 enum toRGB = xyzToRgbMatrix(To.colorSpaceDesc!WorkType);
359                 enum mat = multiply(toXYZ, toRGB);
360                 WorkType[3] v = multiply(mat, [r, g, b]);
361                 r = v[0]; g = v[1]; b = v[2];
362             }
363             static if (To.linear == false)
364             {
365                 r = toGamma!(To.colorSpace)(r);
366                 g = toGamma!(To.colorSpace)(g);
367                 b = toGamma!(To.colorSpace)(b);
368             }
369 
370             // convert and return the output
371             static if (To.hasAlpha)
372                 return To(cast(ToType)r, cast(ToType)g, cast(ToType)b, cast(ToType)src[3]);
373             else
374                 return To(cast(ToType)r, cast(ToType)g, cast(ToType)b);
375         }
376     }
377     unittest
378     {
379         // test RGB format conversions
380         alias UnsignedRGB = RGB!("rgb", ubyte);
381         alias SignedRGBX = RGB!("rgbx", byte);
382         alias FloatRGBA = RGB!("rgba", float);
383 
384         static assert(convertColorImpl!(UnsignedRGB)(SignedRGBX(0x20,0x30,-10)) == UnsignedRGB(0x40,0x60,0));
385         static assert(convertColorImpl!(UnsignedRGB)(FloatRGBA(1,0.5,0,1)) == UnsignedRGB(0xFF,0x80,0));
386         static assert(convertColorImpl!(FloatRGBA)(UnsignedRGB(0xFF,0x80,0)) == FloatRGBA(1,float(0x80)/float(0xFF),0,0));
387         static assert(convertColorImpl!(FloatRGBA)(SignedRGBX(127,-127,-128)) == FloatRGBA(1,-1,-1,0));
388 
389         static assert(convertColorImpl!(UnsignedRGB)(convertColorImpl!(FloatRGBA)(UnsignedRGB(0xFF,0x80,0))) == UnsignedRGB(0xFF,0x80,0));
390 
391         // test greyscale conversion
392         alias UnsignedL = RGB!("l", ubyte);
393         static assert(cast(UnsignedL)UnsignedRGB(0xFF,0x20,0x40) == UnsignedL(82));
394 
395         // TODO: we can't test this properly since DMD can't CTFE the '^^' operator! >_<
396 
397         alias sRGBA = RGB!("rgba", ubyte, false, RGBColorSpace.sRGB);
398 
399         // test linear conversion
400         alias lRGBA = RGB!("rgba", ushort, true, RGBColorSpace.sRGB);
401         assert(convertColorImpl!(lRGBA)(sRGBA(0xFF, 0xFF, 0xFF, 0xFF)) == lRGBA(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF));
402 
403         // test gamma conversion
404         alias gRGBA = RGB!("rgba", byte, false, RGBColorSpace.sRGB_Gamma2_2);
405         assert(convertColorImpl!(gRGBA)(sRGBA(0xFF, 0x80, 0x01, 0xFF)) == gRGBA(0x7F, 0x3F, 0x03, 0x7F));
406     }
407 
408     static To convertColorImpl(To, From)(From color) if (isRGB!From && isXYZ!To)
409     {
410         alias ToType = To.ComponentType;
411         alias FromType = From.ComponentType;
412         alias WorkType = WorkingType!(FromType, ToType);
413 
414         // unpack the working values
415         auto src = color.tristimulus;
416         WorkType r = cast(WorkType)src[0];
417         WorkType g = cast(WorkType)src[1];
418         WorkType b = cast(WorkType)src[2];
419 
420         static if (From.linear == false)
421         {
422             r = toLinear!(From.colorSpace)(r);
423             g = toLinear!(From.colorSpace)(g);
424             b = toLinear!(From.colorSpace)(b);
425         }
426 
427         // transform to XYZ
428         enum toXYZ = rgbToXyzMatrix(From.colorSpaceDesc!WorkType);
429         WorkType[3] v = multiply(toXYZ, [r, g, b]);
430         return To(v[0], v[1], v[2]);
431     }
432     unittest
433     {
434         // TODO: needs approx ==
435     }
436 
437     static To convertColorImpl(To, From)(From color) if (isXYZ!From && isRGB!To)
438     {
439         alias ToType = To.ComponentType;
440         alias FromType = From.ComponentType;
441         alias WorkType = WorkingType!(FromType, ToType);
442 
443         enum toRGB = xyzToRgbMatrix(To.colorSpaceDesc!WorkType);
444         WorkType[3] v = multiply(toRGB, [ WorkType(color.X), WorkType(color.Y), WorkType(color.Z) ]);
445 
446         static if (To.linear == false)
447         {
448             v[0] = toGamma!(To.colorSpace)(v[0]);
449             v[1] = toGamma!(To.colorSpace)(v[1]);
450             v[2] = toGamma!(To.colorSpace)(v[2]);
451         }
452 
453         return To(cast(ToType)v[0], cast(ToType)v[1], cast(ToType)v[2]);
454     }
455     unittest
456     {
457         // TODO: needs approx ==
458     }
459 
460 private:
461     alias AllComponents = TypeTuple!("l","r","g","b","a");
462 }
463 
464 
465 /** Convert a value from gamma compressed space to linear. */
466 T toLinear(RGBColorSpace src, T)(T v) if (isFloatingPoint!T)
467 {
468     enum ColorSpace = rgbColorSpaceDefs!T[src];
469     return ColorSpace.toLinear(v);
470 }
471 /** Convert a value to gamma compressed space. */
472 T toGamma(RGBColorSpace src, T)(T v) if (isFloatingPoint!T)
473 {
474     enum ColorSpace = rgbColorSpaceDefs!T[src];
475     return ColorSpace.toGamma(v);
476 }
477 
478 /** Convert a color to linear space. */
479 auto toLinear(C)(C color) if (isRGB!C)
480 {
481     return cast(RGB!(C.components, C.ComponentType, true, C.colorSpace))color;
482 }
483 /** Convert a color to gamma space. */
484 auto toGamma(C)(C color) if (isRGB!C)
485 {
486     return cast(RGB!(C.components, C.ComponentType, false, C.colorSpace))color;
487 }
488 
489 
490 package:
491 
492 T toGrayscale(bool linear, RGBColorSpace colorSpace = RGBColorSpace.sRGB, T)(T r, T g, T b) pure if (isFloatingPoint!T)
493 {
494     static if (linear)
495     {
496         // calculate the luminance (Y) value correctly by multiplying the Y row of the XYZ matrix with the color
497         enum YAxis = rgbColorSpaceDef!T(colorSpace).rgbToXyzMatrix()[1];
498         return YAxis[0]*r + YAxis[1]*g + YAxis[2]*b;
499     }
500     else static if (colorSpace == RGBColorSpace.Colorimetry ||
501                    colorSpace == RGBColorSpace.NTSC ||
502                    colorSpace == RGBColorSpace.NTSC_J ||
503                    colorSpace == RGBColorSpace.PAL_SECAM)
504     {
505         // For color spaces which are used in standard color TV and video systems such as PAL/SECAM, and
506         // NTSC, a nonlinear luma component (Y') is calculated directly from gamma-compressed primary
507         // intensities as a weighted sum, which can be calculated quickly without the gamma expansion and
508         // compression used in colorimetric grayscale calculations.
509         // The Rec.601 luma (Y') component is computed as:
510         return T(0.299)*r + T(0.587)*g + T(0.114)*b;
511     }
512     else static if (colorSpace == RGBColorSpace.HDTV)
513     {
514         // The Rec.709 standard used for HDTV  uses different color coefficients.
515         // These happen to be the same as sRGB, but applied to the gamma compressed signal direcetly.
516         return T(0.2126)*r + T(0.7152)*g + T(0.0722)*b;
517     }
518     else
519     {
520         // Edge-case: What to do?! Approximate, or perform gamma conversions?
521         // The TV standards have defined approximations, so let's continue to roll with that pattern.
522         // We'll continue the Rec.709 pattern, except using appropriate coefficients for the color space.
523         enum YAxis = rgbColorSpaceDef!T(colorSpace).rgbToXyzMatrix()[1];
524         return YAxis[0]*r + YAxis[1]*g + YAxis[2]*b;
525     }
526 }
527 T toGrayscale(bool linear, RGBColorSpace colorSpace = RGBColorSpace.sRGB, T)(T r, T g, T b) pure if (is(T == NormalizedInt!U, U))
528 {
529     alias F = FloatTypeFor!T;
530     return T(toGrayscale!(linear, colorSpace)(cast(F)r, cast(F)g, cast(F)b));
531 }
532 
533 
534 // helpers to parse color components from color component string
535 template canFind(string s, char c)
536 {
537     static if (s.length == 0)
538         enum canFind = false;
539     else
540         enum canFind = s[0] == c || canFind!(s[1..$], c);
541 }
542 template allIn(string s, string chars)
543 {
544     static if (chars.length == 0)
545         enum allIn = true;
546     else
547         enum allIn = canFind!(s, chars[0]) && allIn!(s, chars[1..$]);
548 }
549 template anyIn(string s, string chars)
550 {
551     static if (chars.length == 0)
552         enum anyIn = false;
553     else
554         enum anyIn = canFind!(s, chars[0]) || anyIn!(s, chars[1..$]);
555 }
556 template notIn(string s, string chars)
557 {
558     static if (chars.length == 0)
559         enum notIn = char(0);
560     else static if (!canFind!(s, chars[0]))
561         enum notIn = chars[0];
562     else
563         enum notIn = notIn!(s, chars[1..$]);
564 }
565 
566 unittest
567 {
568     static assert(canFind!("string", 'i'));
569     static assert(!canFind!("string", 'x'));
570     static assert(allIn!("string", "sgi"));
571     static assert(!allIn!("string", "sgix"));
572     static assert(anyIn!("string", "sx"));
573     static assert(!anyIn!("string", "x"));
574 }