Skip to content

Commit

Permalink
Add Memoizer(Function) and Memoizer(Function, boolean).
Browse files Browse the repository at this point in the history
  • Loading branch information
garydgregory committed Mar 21, 2022
1 parent 85751a1 commit 389fb37
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 17 deletions.
1 change: 1 addition & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="add" dev="ggregory" due-to="Gary Gregory">Add JavaVersion.JAVA_18.</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add TimeZones.toTimeZone(TimeZone).</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add FutureTasks.</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add Memoizer(Function) and Memoizer(Function, boolean).</action>
<!-- UPDATE -->
<action type="update" dev="ggregory" due-to="Dependabot, Gary Gregory">Bump spotbugs-maven-plugin from 4.2.0 to 4.5.0.0 #735, #808, #822, #834.</action>
<action type="update" dev="ggregory" due-to="Dependabot, XenoAmess">Bump actions/cache from v2.1.4 to v2.1.7 #742, #752, #764, #833.</action>
Expand Down
34 changes: 33 additions & 1 deletion src/main/java/org/apache/commons/lang3/concurrent/Memoizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class Memoizer<I, O> implements Computable<I, O> {
* Constructs a Memoizer for the provided Computable calculation.
* </p>
* <p>
* If a calculation is thrown an exception for any reason, this exception will be cached and returned for all future
* If a calculation throws an exception for any reason, this exception will be cached and returned for all future
* calls with the provided parameter.
* </p>
*
Expand All @@ -80,6 +80,38 @@ public Memoizer(final Computable<I, O> computable, final boolean recalculate) {
this.mappingFunction = k -> FutureTasks.run(() -> computable.compute(k));
}

/**
* <p>
* Constructs a Memoizer for the provided Function calculation.
* </p>
* <p>
* If a calculation throws an exception for any reason, this exception will be cached and returned for all future
* calls with the provided parameter.
* </p>
*
* @param function the function whose results should be memorized
* @since 2.13.0
*/
public Memoizer(final Function<I, O> function) {
this(function, false);
}

/**
* <p>
* Constructs a Memoizer for the provided Function calculation, with the option of whether a Function that
* experiences an error should recalculate on subsequent calls or return the same cached exception.
* </p>
*
* @param function the computation whose results should be memorized
* @param recalculate determines whether the computation should be recalculated on subsequent calls if the previous call
* failed
* @since 2.13.0
*/
public Memoizer(final Function<I, O> function, final boolean recalculate) {
this.recalculate = recalculate;
this.mappingFunction = k -> FutureTasks.run(() -> function.apply(k));
}

/**
* <p>
* This method will return the result of the calculation and cache it, if it has not previously been calculated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MemoizerTest {
public class MemoizerComputableTest {

private Computable<Integer, Integer> computable;

Expand All @@ -34,17 +34,6 @@ public void setUpComputableMock() {
computable = EasyMock.mock(Computable.class);
}

@Test
public void testOnlyCallComputableOnceIfDoesNotThrowException() throws Exception {
final Integer input = 1;
final Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
expect(computable.compute(input)).andReturn(input);
replay(computable);

assertEquals(input, memoizer.compute(input), "Should call computable first time");
assertEquals(input, memoizer.compute(input), "Should not call the computable the second time");
}

@Test
public void testDefaultBehaviourNotToRecalculateExecutionExceptions() throws Exception {
final Integer input = 1;
Expand Down Expand Up @@ -83,14 +72,14 @@ public void testDoesRecalculateWhenSetToTrue() throws Exception {
}

@Test
public void testWhenComputableThrowsRuntimeException() throws Exception {
public void testOnlyCallComputableOnceIfDoesNotThrowException() throws Exception {
final Integer input = 1;
final Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
final RuntimeException runtimeException = new RuntimeException("Some runtime exception");
expect(computable.compute(input)).andThrow(runtimeException);
expect(computable.compute(input)).andReturn(input);
replay(computable);

assertThrows(RuntimeException.class, () -> memoizer.compute(input));
assertEquals(input, memoizer.compute(input), "Should call computable first time");
assertEquals(input, memoizer.compute(input), "Should not call the computable the second time");
}

@Test
Expand All @@ -103,4 +92,15 @@ public void testWhenComputableThrowsError() throws Exception {

assertThrows(Error.class, () -> memoizer.compute(input));
}

@Test
public void testWhenComputableThrowsRuntimeException() throws Exception {
final Integer input = 1;
final Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
final RuntimeException runtimeException = new RuntimeException("Some runtime exception");
expect(computable.compute(input)).andThrow(runtimeException);
replay(computable);

assertThrows(RuntimeException.class, () -> memoizer.compute(input));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.lang3.concurrent;

import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.function.Function;

import org.easymock.EasyMock;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MemoizerFunctionTest {

private Function<Integer, Integer> function;

@BeforeEach
public void setUpComputableMock() {
function = EasyMock.mock(Function.class);
}

@Test
public void testDefaultBehaviourNotToRecalculateExecutionExceptions() throws Exception {
final Integer input = 1;
final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function);
final IllegalArgumentException interruptedException = new IllegalArgumentException();
expect(function.apply(input)).andThrow(interruptedException);
replay(function);

assertThrows(Throwable.class, () -> memoizer.compute(input));
assertThrows(IllegalArgumentException.class, () -> memoizer.compute(input));
}

@Test
public void testDoesNotRecalculateWhenSetToFalse() throws Exception {
final Integer input = 1;
final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function, false);
final IllegalArgumentException interruptedException = new IllegalArgumentException();
expect(function.apply(input)).andThrow(interruptedException);
replay(function);

assertThrows(Throwable.class, () -> memoizer.compute(input));
assertThrows(IllegalArgumentException.class, () -> memoizer.compute(input));
}

@Test
public void testDoesRecalculateWhenSetToTrue() throws Exception {
final Integer input = 1;
final Integer answer = 3;
final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function, true);
final IllegalArgumentException interruptedException = new IllegalArgumentException();
expect(function.apply(input)).andThrow(interruptedException).andReturn(answer);
replay(function);

assertThrows(Throwable.class, () -> memoizer.compute(input));
assertEquals(answer, memoizer.compute(input));
}

@Test
public void testOnlyCallComputableOnceIfDoesNotThrowException() throws Exception {
final Integer input = 1;
final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function);
expect(function.apply(input)).andReturn(input);
replay(function);

assertEquals(input, memoizer.compute(input), "Should call computable first time");
assertEquals(input, memoizer.compute(input), "Should not call the computable the second time");
}

@Test
public void testWhenComputableThrowsError() throws Exception {
final Integer input = 1;
final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function);
final Error error = new Error();
expect(function.apply(input)).andThrow(error);
replay(function);

assertThrows(Error.class, () -> memoizer.compute(input));
}

@Test
public void testWhenComputableThrowsRuntimeException() throws Exception {
final Integer input = 1;
final Memoizer<Integer, Integer> memoizer = new Memoizer<>(function);
final RuntimeException runtimeException = new RuntimeException("Some runtime exception");
expect(function.apply(input)).andThrow(runtimeException);
replay(function);

assertThrows(RuntimeException.class, () -> memoizer.compute(input));
}
}

0 comments on commit 389fb37

Please sign in to comment.