Skip to content

Commit 5ccba09

Browse files
committed
fix: add non-finite value validation to time system and limit tempo editor to beat-based time bases
1 parent 3291311 commit 5ccba09

File tree

10 files changed

+103
-3
lines changed

10 files changed

+103
-3
lines changed

blue-core/src/main/java/blue/time/TimeDuration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,9 @@ public static final class DurationSeconds extends TimeDuration {
588588
private final double totalSeconds;
589589

590590
public DurationSeconds(double totalSeconds) {
591+
if (!Double.isFinite(totalSeconds)) {
592+
throw new IllegalArgumentException("Duration seconds must be finite: " + totalSeconds);
593+
}
591594
if (totalSeconds < 0) {
592595
throw new IllegalArgumentException("Duration seconds cannot be negative: " + totalSeconds);
593596
}

blue-core/src/main/java/blue/time/TimePosition.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,9 @@ public static final class SecondsValue extends TimePosition {
694694
private final double totalSeconds;
695695

696696
public SecondsValue(double totalSeconds) {
697+
if (!Double.isFinite(totalSeconds)) {
698+
throw new IllegalArgumentException("Seconds must be finite: " + totalSeconds);
699+
}
697700
if (totalSeconds < 0) {
698701
throw new IllegalArgumentException("Seconds cannot be negative: " + totalSeconds);
699702
}

blue-core/src/test/java/blue/time/TimeDurationTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,19 @@ void testDurationSecondsNegativeRejected() {
384384
TimeDuration.seconds(-0.001);
385385
});
386386
}
387+
388+
@Test
389+
void testDurationSecondsNonFiniteRejected() {
390+
assertThrows(IllegalArgumentException.class, () -> {
391+
TimeDuration.seconds(Double.NaN);
392+
});
393+
assertThrows(IllegalArgumentException.class, () -> {
394+
TimeDuration.seconds(Double.POSITIVE_INFINITY);
395+
});
396+
assertThrows(IllegalArgumentException.class, () -> {
397+
TimeDuration.seconds(Double.NEGATIVE_INFINITY);
398+
});
399+
}
387400

388401
// ========== DurationFrames Tests ==========
389402

blue-core/src/test/java/blue/time/TimePositionTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,19 @@ void testSecondsValueNegativeRejected() {
327327
});
328328
}
329329

330+
@Test
331+
void testSecondsValueNonFiniteRejected() {
332+
assertThrows(IllegalArgumentException.class, () -> {
333+
TimePosition.seconds(Double.NaN);
334+
});
335+
assertThrows(IllegalArgumentException.class, () -> {
336+
TimePosition.seconds(Double.POSITIVE_INFINITY);
337+
});
338+
assertThrows(IllegalArgumentException.class, () -> {
339+
TimePosition.seconds(Double.NEGATIVE_INFINITY);
340+
});
341+
}
342+
330343
@Test
331344
void testSecondsValueXMLRoundTrip() throws Exception {
332345
TimePosition original = TimePosition.seconds(12.345678);

blue-ui-core/src/main/java/blue/ui/core/score/tempo/TempoRegionBar.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,8 @@ private void showEditTempoDialog(int regionIndex) {
349349
JPanel positionSection = new JPanel(new BorderLayout(5, 5));
350350
positionSection.setBorder(BorderFactory.createTitledBorder("Position"));
351351

352-
SoundObjectTimePanel timePanel = new SoundObjectTimePanel();
352+
SoundObjectTimePanel timePanel = new SoundObjectTimePanel(
353+
TimeBase.BEATS, TimeBase.BBT, TimeBase.BBST, TimeBase.BBF);
353354
// Use the actual TimePosition from the tempo point (preserves Measure:Beats if that's how it was entered)
354355
timePanel.setTimePosition(currentPosition);
355356
timePanel.setTimeBaseSelectionEnabled(true);

blue-ui-core/src/main/java/blue/ui/core/time/SoundObjectTimePanel.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,15 @@ public class SoundObjectTimePanel extends JPanel {
5353
* Creates a new SoundObjectTimePanel.
5454
*/
5555
public SoundObjectTimePanel() {
56+
this(TimeBase.values());
57+
}
58+
59+
public SoundObjectTimePanel(TimeBase... timeBases) {
5660
propertyChangeSupport = new PropertyChangeSupport(this);
5761
setLayout(new BorderLayout());
5862

5963
// TimeBase selector in NORTH
60-
timeBaseSelector = new TimeBaseSelector();
64+
timeBaseSelector = new TimeBaseSelector(timeBases);
6165
add(timeBaseSelector, BorderLayout.NORTH);
6266

6367
// Unified text editor for all formats

blue-ui-core/src/main/java/blue/ui/core/time/TimeBaseSelector.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ public class TimeBaseSelector extends JComboBox<TimeBase> {
3838
* Creates a new TimeBaseSelector with all available TimeBases.
3939
*/
4040
public TimeBaseSelector() {
41-
super(TimeBase.values());
41+
this(TimeBase.values());
42+
}
43+
44+
public TimeBaseSelector(TimeBase... timeBases) {
45+
super(timeBases);
4246
setRenderer(new TimeBaseRenderer());
4347
// Prevent overly wide preferred/min sizes driven by long display strings
4448
setPrototypeDisplayValue(TimeBase.BBST);

blue-ui-core/src/main/java/blue/ui/core/time/TimeUnitTextField.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,9 @@ private static TimePosition parseSeconds(String text) {
510510
return TimePosition.SecondsValue.ZERO;
511511
}
512512
double seconds = Double.parseDouble(text);
513+
if (!Double.isFinite(seconds)) {
514+
throw new IllegalArgumentException("Seconds must be finite");
515+
}
513516
if (seconds < 0) {
514517
throw new IllegalArgumentException("Seconds cannot be negative");
515518
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* blue - object composition environment for csound
3+
* Copyright (C) 2026
4+
* Steven Yi <stevenyi@gmail.com>
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU General Public License
8+
* as published by the Free Software Foundation; either version 2
9+
* of the License or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program; if not, write to the Free Software
18+
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19+
*/
20+
package blue.ui.core.time;
21+
22+
import blue.time.TimeBase;
23+
import static org.junit.jupiter.api.Assertions.assertEquals;
24+
import org.junit.jupiter.api.Test;
25+
26+
class SoundObjectTimePanelTest {
27+
28+
@Test
29+
void testConstructorCanRestrictAvailableTimeBases() {
30+
SoundObjectTimePanel panel = new SoundObjectTimePanel(
31+
TimeBase.BEATS, TimeBase.BBT, TimeBase.BBST, TimeBase.BBF);
32+
33+
TimeBaseSelector selector = (TimeBaseSelector) panel.getComponent(0);
34+
35+
assertEquals(4, selector.getItemCount());
36+
assertEquals(TimeBase.BEATS, selector.getItemAt(0));
37+
assertEquals(TimeBase.BBT, selector.getItemAt(1));
38+
assertEquals(TimeBase.BBST, selector.getItemAt(2));
39+
assertEquals(TimeBase.BBF, selector.getItemAt(3));
40+
}
41+
}

blue-ui-core/src/test/java/blue/ui/core/time/TimeUnitTextFieldTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ void testParseSeconds() {
8989
assertEquals(1.5, parsed.getTotalSeconds(), 0.0001);
9090
}
9191

92+
@Test
93+
void testParseSecondsRejectsNonFiniteValue() {
94+
TimeUnitTextField field = new TimeUnitTextField();
95+
field.setTimeBase(TimeBase.SECONDS);
96+
field.setTimePosition(TimePosition.seconds(1.5));
97+
field.setText("NaN");
98+
99+
field.postActionEvent();
100+
101+
TimePosition.SecondsValue parsed = assertInstanceOf(TimePosition.SecondsValue.class,
102+
field.getTimePosition());
103+
assertEquals(1.5, parsed.getTotalSeconds(), 0.0001);
104+
assertEquals("1.5", field.getText());
105+
}
106+
92107
@Test
93108
void testFormatDurationSeconds() {
94109
TimeUnitTextField field = new TimeUnitTextField();

0 commit comments

Comments
 (0)