Skip to content

Commit

Permalink
refactor: Extract binary digits logic
Browse files Browse the repository at this point in the history
The "business logic" for determining the digits corresponding to
the current time was closely coupled to the rendering logic.

This commit extracts the digit computation logic, primarily to
facilitate unit testing, but it has the side benefit of making the
code easier to read.
  • Loading branch information
thehale committed Jun 23, 2024
1 parent 6b92798 commit f24edd3
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 95 deletions.
16 changes: 0 additions & 16 deletions __tests__/App-test.tsx

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@rnx-kit/cli": "^0.16.8",
"@rnx-kit/metro-config": "^1.3.6",
"@rnx-kit/metro-resolver-symlinks": "^0.1.28",
"@types/jest": "^29.5.12",
"@types/react": "^18.2.6",
"@types/react-native": "^0.67.3",
"@types/react-native-keep-awake": "^2.0.3",
Expand Down
65 changes: 18 additions & 47 deletions src/components/BinaryClock.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// Copyright (c) 2022 Joseph Hale
// Copyright (c) 2022-2024 Joseph Hale
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

import React, {useEffect, useState} from 'react';
import {BinaryTimeMode, asBinaryTime} from '../utils/binaryTime';
import {StyleSheet, View} from 'react-native';

import BinaryDigit from './BinaryDigit';
import Orientation from '../utils/orientation';
import {useTime} from '../utils/useTime';

interface BinaryClockProps {
orientation?: Orientation;
Expand All @@ -26,48 +27,25 @@ const DEFAULTS = {

const BinaryClock: React.FC<BinaryClockProps> = args => {
const props = {...DEFAULTS, ...args};

const [time, setTime] = useState(new Date());
useEffect(() => {
const toggle = setInterval(() => {
setTime(new Date());
}, 50);
return () => clearInterval(toggle);
});

let digits = [];
let hours = time.getHours();
let minutes = time.getMinutes();
let seconds = time.getSeconds();
switch (props.orientation) {
case Orientation.Portrait:
digits = [
{key: 'h', value: hours, maxVisible: 23},
{key: 'm', value: minutes, maxVisible: 59},
{key: 's', value: seconds, maxVisible: 59},
];
break;
case Orientation.Landscape:
digits = [
{key: 'h10', value: firstDigit(hours), maxVisible: 2},
{key: 'h1', value: secondDigit(hours), maxVisible: 9},
{key: 'm10', value: firstDigit(minutes), maxVisible: 5},
{key: 'm1', value: secondDigit(minutes), maxVisible: 9},
{key: 's10', value: firstDigit(seconds), maxVisible: 5},
{key: 's1', value: secondDigit(seconds), maxVisible: 9},
];
break;
}
let maxValue = Math.max(...digits.map(digit => digit.maxVisible));
const time = useTime();
const binaryTime = asBinaryTime(
{
hours: time.getHours(),
minutes: time.getMinutes(),
seconds: time.getSeconds(),
},
{
[Orientation.Portrait]: BinaryTimeMode.SINGLE_DIGITS,
[Orientation.Landscape]: BinaryTimeMode.DOUBLE_DIGITS,
}[props.orientation],
);

return (
<View style={styles.binaryClock}>
{digits.map(digit => (
{binaryTime.digits.map((digit, idx) => (
<BinaryDigit
key={digit.key}
value={digit.value}
maxVisible={digit.maxVisible}
maxValue={maxValue}
key={idx}
digit={digit}
brightness={props.brightness}
roundness={props.roundness}
showHints={props.showHints}
Expand All @@ -77,13 +55,6 @@ const BinaryClock: React.FC<BinaryClockProps> = args => {
);
};

function firstDigit(value: number) {
return Math.floor(value / 10);
}
function secondDigit(value: number) {
return value % 10;
}

const styles = StyleSheet.create({
binaryClock: {
flexDirection: 'row',
Expand Down
27 changes: 7 additions & 20 deletions src/components/BinaryDigit.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Joseph Hale
// Copyright (c) 2022-2024 Joseph Hale
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
Expand All @@ -8,49 +8,36 @@ import {StyleSheet, Text, View} from 'react-native';

import BinaryDot from './BinaryDot';
import React from 'react';
import { type BinaryDigit as BinaryDigitType } from '../utils/binaryTime';

interface BinaryDigitProps {
value: number;
digit: BinaryDigitType
brightness?: number;
roundness?: number;
maxVisible?: number;
maxValue?: number;
showHints?: boolean;
}

const DEFAULTS = {
brightness: 1,
roundness: 1,
maxVisible: 15,
maxValue: 15,
showHints: false,
};

/* eslint no-bitwise: ["error", { "allow": ["&"] }] */
const BinaryDigit: React.FC<BinaryDigitProps> = args => {
const props = {...DEFAULTS, ...args};

let dotCount = 0;
if (props.maxValue > 0) {
dotCount = Math.floor(Math.log2(props.maxValue)) + 1;
}
let dotValues = Array.from(Array(dotCount), (_, i) => 2 ** i);

return (
<View style={styles.pair}>
<View style={styles.digit}>
{dotValues.reverse().map(value => (
{props.digit.bits.map((bit, idx) => (
<BinaryDot
key={value}
active={(props.value & value) > 0}
visible={props.maxVisible >= value}
value={value}
key={idx}
bit={bit}
brightness={props.brightness}
roundness={props.roundness}
showHints={props.showHints}
/>
))}
{props.showHints && <Text style={styles.hint}>{props.value}</Text>}
{props.showHints && <Text style={styles.hint}>{props.digit.value}</Text>}
</View>
</View>
);
Expand Down
17 changes: 7 additions & 10 deletions src/components/BinaryDot.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
// Copyright (c) 2022 Joseph Hale
// Copyright (c) 2022-2024 Joseph Hale
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

import {StyleSheet, Text, View, ViewStyle} from 'react-native';

import {BinaryBit} from '../utils/binaryTime';
import React from 'react';

interface BinaryDotProps {
active: boolean;
visible?: boolean;
value?: number;
bit: BinaryBit;
brightness?: number;
roundness?: number;
showHints?: boolean;
}

const DEFAULTS = {
visible: true,
value: 1,
brightness: 1,
roundness: 1,
showHints: false,
Expand All @@ -30,18 +27,18 @@ const FULL_ROUNDNESS_RADIUS = 30;
const BinaryDot: React.FC<BinaryDotProps> = args => {
const props = {...DEFAULTS, ...args};

let active_modifier = props.active ? 1 : 0.25;
let visible_modifier = props.visible ? 1 : 0;
let active_modifier = props.bit.active ? 1 : 0.25;
let visible_modifier = props.bit.visible ? 1 : 0;
const overrides: ViewStyle = {
opacity: props.brightness * active_modifier * visible_modifier,
borderRadius: props.roundness * FULL_ROUNDNESS_RADIUS,
};

return (
<View style={[styles.dot, overrides]}>
{props.showHints && props.value && (
{props.showHints && props.bit.value && (
<View style={styles.hint}>
<Text style={styles.hintText}>{props.value}</Text>
<Text style={styles.hintText}>{props.bit.value}</Text>
</View>
)}
</View>
Expand Down
94 changes: 94 additions & 0 deletions src/utils/__tests__/__fixtures__/toRepresentBinary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) 2024 Joseph Hale
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import {type BinaryBit, type BinaryDigit} from '../../binaryTime';

declare module 'expect' {
interface AssymmetricMatchers {
toRepresentBinary(templates: string | string[]): void
}
interface Matchers<R> {
toRepresentBinary(templates: string | string[]): R
}
}
expect.extend({toRepresentBinary});

function toRepresentBinary(
actual: BinaryDigit | BinaryDigit[],
templates: string | string[],
) {
const _digits = Array.isArray(actual) ? actual : [actual];
const _templates = Array.isArray(templates) ? templates : [templates];
if (_digits.length != _templates.length) {
return {
message: () =>
`expected: ${_templates.length} digits for '${JSON.stringify(
_templates,
)}', was: ${_digits.length} digits in ${JSON.stringify(_digits)}`,
pass: false,
};
}
for (let idx = 0; idx < _templates.length; idx++) {
const result = _toRepresentBinary(_digits[idx].bits, _templates[idx]);
if (result) return result;
}
return {
message: () => `expected '${_digits}' to represent '${_templates}'`,
pass: true,
};
}
function _toRepresentBinary(actual: BinaryBit[], template: string) {
const _bits = actual;
if (_bits.length != template.length) {
return {
message: () =>
`expected: ${template.length} bits for '${template}', was: ${
_bits.length
} bits in ${JSON.stringify(_bits)}`,
pass: false,
};
} else {
const msg = (idx: number) =>
`for template '${template}', bit ${idx + 1}/${_bits.length} in ${_bits}`;
for (let idx = 0; idx < template.length; idx++) {
switch (template[idx]) {
case ' ':
if (visibility(_bits[idx]) != 'invisible') {
return {message: () => msg(idx), pass: false};
}
break;
case '0':
if (activeStatus(_bits[idx]) != 'inactive') {
return {message: () => msg(idx), pass: false};
}
if (visibility(_bits[idx]) != 'visible') {
return {message: () => msg(idx), pass: false};
}
break;
case '1':
if (activeStatus(_bits[idx]) != 'active') {
return {message: () => msg(idx), pass: false};
}
if (visibility(_bits[idx]) != 'visible') {
return {message: () => msg(idx), pass: false};
}
break;
default:
throw new Error(
`A binary template cannot contain '${template[idx]}'`,
);
}
}
}
}

function activeStatus(bit: BinaryBit): string {
return bit.active ? 'active' : 'inactive';
}

function visibility(bit: BinaryBit): string {
return bit.visible ? 'visible' : 'invisible';
}
Loading

0 comments on commit f24edd3

Please sign in to comment.