Skip to content

Commit 18d858e

Browse files
Flip mix(a, b, w) weight semantics (#59)
`w=0` now returns `a`, and `w=1` returns `b`. Update tests, implementation, and documentation.
1 parent 564dffd commit 18d858e

7 files changed

Lines changed: 27 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
## [0.16.1] - 2026-04-10
2+
3+
### Changed
4+
- **`mix(a, b, w)` weight semantics flipped**: `w=0` now returns `a` and `w=1` returns `b`, matching the conventional linear interpolation convention. Previously the semantics were reversed.
5+
16
## [0.16.0] - 2026-04-06
27

38
### Added
49
- **Start time function expressions** (#48): New composable functions for constraining and blending solar and fixed times in start time expressions. All arguments may be fixed times, sun keywords (with optional offset), or nested function calls.
510
- `notBefore(expr, min)` — ensures the result is not earlier than `min`; returns the latter of the two. Alias: `max(a, b)`.
611
- `notAfter(expr, max)` — ensures the result is not later than `max`; returns the earlier of the two. Alias: `min(a, b)`.
712
- `clamp(expr, min, max)` — constrains the result to the `[min, max]` range.
8-
- `mix(a, b, weight)` — linearly interpolates between two times with the given `weight` in `[0..1]` or as a percentage (`0%..100%`). A weight of `1` returns `a`, `0` returns `b`. Example: `mix(sunrise, 08:00, 0.35)` blends toward a fixed anchor, softening seasonal drift.
13+
- `mix(a, b, weight)` — linearly interpolates between two times with the given `weight` in `[0..1]` or as a percentage (`0%..100%`). Example: `mix(sunrise, 08:00, 0.35)` blends toward a fixed anchor, softening seasonal drift.
914
- `smooth(expr, halfLife)` — exponentially smooths a time expression over past days to reduce day-to-day variation. `halfLife` specifies how many days it takes for the smoothing influence to halve (e.g. `14d`). Useful for softening sharp seasonal sunrise/sunset swings. Example: `smooth(sunrise, 14d)`.
1015
- Functions are case-insensitive and fully composable, e.g. `clamp(smooth(sunrise, 14d), 07:00, 09:00)`.
1116
- Full documentation is available in the [Light Configuration docs](/docs/light_configuration.md#constraint-functions).

docs/light_configuration.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,13 @@ Bedroom clamp(smooth(sunrise, 14d), 06:30, 08:00) bri:30%
126126

127127
#### Experimental: `mix(...)` — blend two time expressions
128128

129-
`mix(a, b, w)` places the trigger time between two time expressions `a` and `b`. The weight `w` controls how close the result is to `a`:
129+
`mix(a, b, w)` places the trigger time between two time expressions `a` and `b`. The weight `w` controls how close the result is to `b`:
130130

131-
- `w = 1` → exactly `a`
132-
- `w = 0` → exactly `b`
131+
- `w = 0` → exactly `a`
132+
- `w = 1` → exactly `b`
133133
- `w = 0.5` → midpoint between `a` and `b`
134134

135-
A common use is blending a solar time with a fixed clock time: `mix(sunrise, 07:30, 0.35)` takes sunrise but pulls it 65% toward `07:30` — so the schedule still moves with the seasons, but much more gently. You can also blend two solar times directly: `mix(golden_hour, sunset, 0.5)`. The order of `a` and `b` does not matter — the result is always between the two.
135+
A common use is blending a solar time with a fixed clock time: `mix(sunrise, 07:30, 0.35)` takes sunrise but pulls it 35% toward `07:30` — so the schedule still moves with the seasons, but much more gently. The result is always between the two endpoints. You can also blend two solar times directly: `mix(golden_hour, sunset, 0.5)`.
136136

137137
**Why use it:** If you like solar-based schedules but want them to behave more like a stable routine (e.g., "around 07:30, but still season-aware"), `mix` **narrows the seasonal range**. However, since both values are recalculated fresh each day, day-to-day jumps are not smoothed out.
138138

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>at.sv.hue</groupId>
88
<artifactId>hue-scheduler</artifactId>
9-
<version>0.16.0</version>
9+
<version>0.16.1-SNAPSHOT</version>
1010

1111
<scm>
1212
<connection>scm:git:git@github.com:stefanvictora/hue-scheduler.git</connection>

src/main/java/at/sv/hue/HueScheduler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161

6262
import static at.sv.hue.InputConfigurationParser.parseBrightnessPercentValue;
6363

64-
@Command(name = "HueScheduler", version = "0.16.0", mixinStandardHelpOptions = true, sortOptions = false)
64+
@Command(name = "HueScheduler", version = "0.16.1", mixinStandardHelpOptions = true, sortOptions = false)
6565
public final class HueScheduler implements Runnable {
6666

6767
private static final Logger LOG = LoggerFactory.getLogger(HueScheduler.class);

src/main/java/at/sv/hue/time/StartTimeProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ public interface StartTimeProvider {
1313
* </ul>
1414
* Supported function names are {@code notBefore}, {@code notAfter}, {@code clamp}, {@code min}, {@code max},
1515
* {@code mix}, and {@code smooth}. The {@code mix(a,b,w)} function interpolates between {@code a} and {@code b}
16-
* with weight {@code w} where {@code w} can be in {@code [0..1]} or as percentage ({@code 0%..100%}).
16+
* with weight {@code w} (in {@code [0..1]} or as percentage {@code 0%..100%}), where {@code w=0} returns {@code a}
17+
* and {@code w=1} returns {@code b}.
1718
* The {@code smooth(expr,halfLife)} function exponentially smooths a time expression across past days,
1819
* where {@code halfLife} is specified in days (e.g. {@code 14d}).
1920
* @param dateTime the date to use a reference for resolving solar times

src/main/java/at/sv/hue/time/StartTimeProviderImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ private ZonedDateTime parseFunctionExpression(String input, ZonedDateTime dateTi
9494
ZonedDateTime a = getStart(args.get(0).trim(), dateTime);
9595
ZonedDateTime b = getStart(args.get(1).trim(), dateTime);
9696
double weight = parseMixWeight(args.get(2).trim());
97-
long mixedEpochSeconds = Math.round(a.toEpochSecond() * weight + b.toEpochSecond() * (1.0 - weight));
97+
long mixedEpochSeconds = Math.round(a.toEpochSecond() * (1.0 - weight) + b.toEpochSecond() * weight);
9898
return ZonedDateTime.ofInstant(java.time.Instant.ofEpochSecond(mixedEpochSeconds), a.getZone());
9999
}
100100
case "smooth" -> {

src/test/java/at/sv/hue/time/StartTimeProviderTest.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,8 @@ void notAfter_usesCorrectDate() {
464464

465465
@Test
466466
void mix_withDecimalWeight_interpolatesTimes() {
467-
// mix(sunrise=07:00, 08:00, 0.25) = 07:45
468-
assertStart("mix(sunrise, 08:00, 0.25)", now.with(LocalTime.of(7, 45)));
467+
// mix(sunrise=07:00, 08:00, 0.25) = 07:15
468+
assertStart("mix(sunrise, 08:00, 0.25)", now.with(LocalTime.of(7, 15)));
469469
}
470470

471471
@Test
@@ -486,31 +486,31 @@ void mix_invertedParameters_stillBlends() {
486486
}
487487

488488
@Test
489-
void mix_aLaterThanB_blendsPullingTowardB() {
489+
void mix_aLaterThanB_blendsPullingTowardA() {
490490
// Simulates winter: sunrise=07:00, anchor=06:30 (sunrise passed the anchor)
491-
// mix(07:00, 06:30, 0.35) = 07:00*0.35 + 06:30*0.65 = 06:40:30
492-
assertStart("mix(sunrise, 06:30, 0.35)", now.with(LocalTime.of(6, 40, 30)));
491+
// mix(07:00, 06:30, 0.35) = 07:00*0.65 + 06:30*0.35 = 06:49:30
492+
assertStart("mix(sunrise, 06:30, 0.35)", now.with(LocalTime.of(6, 49, 30)));
493493
}
494494

495495
@Test
496496
void mix_withPercentageWeight_interpolatesTimes() {
497-
// mix(sunrise=07:00, 08:00, 25%) = 07:45
498-
assertStart("mix(sunrise, 08:00, 25%)", now.with(LocalTime.of(7, 45)));
497+
// mix(sunrise=07:00, 08:00, 25%) = 07:15
498+
assertStart("mix(sunrise, 08:00, 25%)", now.with(LocalTime.of(7, 15)));
499499
}
500500

501501
@Test
502-
void mix_withWeightOne_returnsFirstArgument() {
503-
assertStart("mix(sunrise, 08:00, 1)", sunrise);
502+
void mix_withWeightOne_returnsSecondArgument() {
503+
assertStart("mix(sunrise, 08:00, 1)", now.with(LocalTime.of(8, 0)));
504504
}
505505

506506
@Test
507-
void mix_withWeightZero_returnsSecondArgument() {
508-
assertStart("mix(sunrise, 08:00, 0)", now.with(LocalTime.of(8, 0)));
507+
void mix_withWeightZero_returnsFirstArgument() {
508+
assertStart("mix(sunrise, 08:00, 0)", sunrise);
509509
}
510510

511511
@Test
512512
void mix_canBeComposedWithClamp() {
513-
assertStart("clamp(mix(sunrise, 08:00, 0.35), 06:30, 08:00)", now.with(LocalTime.of(7, 39)));
513+
assertStart("clamp(mix(sunrise, 08:00, 0.35), 06:30, 08:00)", now.with(LocalTime.of(7, 21)));
514514
}
515515

516516
@Test

0 commit comments

Comments
 (0)