【问题标题】:Sort Tenors in finance notation using Java使用 Java 对金融符号中的 Tenor 进行排序
【发布时间】:2021-06-29 10:50:14
【问题描述】:

我有男高音列表

List<String> tenors =Arrays.asList("SPOT","1W","2W","10Y", "15Y", "1M", "1Y", "20Y", "2Y", "30Y", "3M", "5Y", "6M", "9M")

其中 M 代表月,Y 代表年。正确排序的顺序(升序)将是

["SPOT","1W","2W","1M", "3M", "6M", "9M", "1Y", "2Y", "5Y", "10Y", "15Y", "20Y", "30Y"]

如何使用 Java 实现这一点?我需要使用自定义比较器吗?

【问题讨论】:

  • 那些应该是字符串吗?您正在使用单引号,这不是有效的 Java 语法。首先,我会避免使用字符串,而是创建一个 Tenor 类,它实现了Comparable&lt;Tenor&gt;
  • 是的,我修改为双引号..
  • 感谢您的澄清,但我的主要建议仍然是——在创建自定义类时不要过度使用字符串会更有意义。否则,这要么是一种反模式,要么至少是一种常见的代码“气味”
  • 很好。此查询有任何可能的解决方案吗?

标签: java comparator


【解决方案1】:

试试这个。

static final Pattern D = Pattern.compile("(\\d+)([WMY])");
static final Map<String, Integer> R = Map.of("W", 7, "M", 30, "Y", 365);

static int days(String s) {
    Matcher m = D.matcher(s);
    return m.matches() ? Integer.parseInt(m.group(1)) * R.get(m.group(2)) : 0;
}

public static void main(String[] args) {
    List<String> tenors = Arrays.asList("SPOT", "1W", "2W", "10Y", "15Y", "1M", "1Y", "20Y", "2Y", "30Y", "3M", "5Y", "6M", "9M");
    Collections.sort(tenors, Comparator.comparingInt(s -> days(s)));
    System.out.println(tenors);
}

输出:

[SPOT, 1W, 2W, 1M, 3M, 6M, 9M, 1Y, 2Y, 5Y, 10Y, 15Y, 20Y, 30Y]

【讨论】:

  • 值得注意的是,您已经对一个月和一年的长度做出了不固定长度的假设(这是解决此问题的唯一方法,但仍然如此)。您的方法还将 30W 和 7M 视为等效的,因此您最终可能会得到不确定的输出,具体取决于输入顺序。在生产系统中,您不希望任意交换这两个位置,因此明智的做法是以天为单位使用长度作为决胜局:例如如果天数相等,M 总是在 W 之后。
  • (30W 和 7M 不是很常见的例子,但 30D 和 1M 肯定是。因此,如果您将其扩展为包括 D = 1,则更有可能发生问题)
【解决方案2】:

一种使用自定义比较器对字符串进行排序的方法(如果您不想创建 Tenor 类):

List<String> tenors =Arrays.asList("SPOT","1W","2W","10Y", "15Y", "1M", "1Y", "20Y", "2Y", "30Y", "3M", "5Y", "6M", "9M");

//Define an order which enables you to sort by value, ex. W=1 < M=2
Map<String, Integer> map = Map.of("SPOT", 0, "W", 1, "M", 2, "Y", 3);

//remove digits to compare only the time-unit part
Comparator<String> byTime = Comparator.comparing(s -> map.get(s.replaceAll("\\d+","")));

//do oposite of the above: remove all non digit chars to compare the values
Comparator<String> byValue = Comparator.comparingInt(s -> Integer.parseInt(s.replaceAll("\\D+","")));

//chain comparators to sort as desired
tenors.sort(byTime.thenComparing(byValue));

System.out.println(tenors);

【讨论】:

    【解决方案3】:

    解决的办法是不要给 Strings 太多的权力,而是创建一个可以保存值的类,例如 Strings,而是带有信息的值,可以使用的信息,包括用于比较彼此的信息。

    例如,在这种情况下,我会创建一个 Tenor 类,它包含一个 int 值以及表示持续时间(例如,周、月、年)的东西。现在我可以使用字符串来表示持续时间,但由于只有有限的值可以工作,所以使用枚举来表示持续时间会更好也更更安全。因此,例如,可以像这样创建一个枚举:

    public enum Duration {
        SPOT("SPOT", 0), WEEK("W", 1), MONTH("M", 4), YEAR("Y", 52);    
        
        private Duration(String abbreviation, int weeks) {
            this.abbreviation = abbreviation;
            this.weeks = weeks;
        }
    
        private String abbreviation;
        private int weeks;
        
        public String getAbbreviation() {
            return abbreviation;
        }
        
        public int getWeeks() {
            return weeks;
        }
    }
    

    代表我们的持续时间。然后 Tenor 字段看起来像:

    public class Tenor {
        private int value;
        private Duration duration;
        
    

    使用 getter、setter、toString 等...

    但我们也想让 Tenor 实现 Comparable 接口,或者更具体地说,Comparable&lt;Tenor&gt; 接口,所以它可能看起来像这样:

    public class Tenor implements Comparable<Tenor> {
        private int value;
        private Duration duration;
        
        public Tenor(Duration duration) {
            this(duration, 0);
        }
        
        public Tenor(Duration duration, int value) {
            this.duration = duration;
            this.value = value;
        }
    
        @Override
        public int compareTo(Tenor o) {
            int weeks = value * duration.getWeeks();
            int otherWeeks = o.value * o.duration.getWeeks();
            return Integer.compare(weeks, otherWeeks);
        }
        
        // getters, setters, toString...
        
    }
    

    在这里,我们可以计算 this Tenor 以及另一个 o Tenor 参数的周数,并使用 Integer.compare(...) 直接比较它们

    我还想要一个方法,也许是 Tenor 类的静态方法,可以将 String 转换为 Tenor。这会有点棘手,并且需要使用正则表达式,以及抛出异常,如果转换出错,最好是“检查”异常。

    可测试的版本(尚未使用检查的异常)可能如下所示:

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class TenorTest {
        public static void main(String[] args) {
            String[] tests = {"SPOT", "1W","2W","10Y", "15Y", "1M", "1Y", "20Y", "2Y", "30Y", "3M", "5Y", "6M", "9M"};
            
            List<Tenor> tenors = new ArrayList<>();
            for (String text : tests) {
                tenors.add(Tenor.toTenor(text));
            }
            
            System.out.println(tenors); 
            
            // sort the collection and then repeat the printout
            Collections.sort(tenors);
            System.out.println(tenors);
        }
    }
    
    public class Tenor implements Comparable<Tenor> {
        private int value;
        private Duration duration;
        
        public Tenor(Duration duration) {
            this(duration, 0);
        }
        
        public Tenor(Duration duration, int value) {
            this.duration = duration;
            this.value = value;
        }
    
        @Override
        public int compareTo(Tenor o) {
            int weeks = value * duration.getWeeks();
            int otherWeeks = o.value * o.duration.getWeeks();
            return Integer.compare(weeks, otherWeeks);
        }
        
        public Duration getDuration() {
            return duration;
        }
        
        public int getValue() {
            return value;
        }
        
        @Override
        public String toString() {
            if (duration == Duration.SPOT) {
                return Duration.SPOT.getAbbreviation();
            } else {
                return Integer.toString(value) + duration.getAbbreviation();
            }
        }
        
        public static Tenor toTenor(String text) {
            Tenor tenor = null;
            int value = -1;
            Duration duration = null;
            text = text.trim().toUpperCase();
            String regex = "(?<=\\d)(?=\\D)";
            String[] tokens = text.split(regex);
            if (tokens.length == 1) {
                if (tokens[0].equals(Duration.SPOT.getAbbreviation())) {
                    tenor = new Tenor(Duration.SPOT, 0);
                } 
            } else if (tokens.length == 2) {
                try {
                    value = Integer.parseInt(tokens[0]);
                    for (Duration d : Duration.values()) {
                        if (d.getAbbreviation().equals(tokens[1])) {
                            duration = d;
                        }
                    }
                    
                    if (duration != null) {
                        tenor = new Tenor(duration, value);
                    }
                    
                } catch (NumberFormatException e) {
                    // ignore
                }
            }
            
            if (tenor != null) {
                return tenor;
            } else {
                // TODO: change this to a custom checked exception
    
                String txt = "For text \"" + text + "\"";
                throw new IllegalArgumentException(txt);
            }
        }
    }
    

    所以,在运行这个程序时,我看到了这个输出:

    [SPOT, 1W, 2W, 10Y, 15Y, 1M, 1Y, 20Y, 2Y, 30Y, 3M, 5Y, 6M, 9M]
    [SPOT, 1W, 2W, 1M, 3M, 6M, 9M, 1Y, 2Y, 5Y, 10Y, 15Y, 20Y, 30Y]
    

    第一行显示未排序的输出,第二行显示排序后的输出

    【讨论】:

      【解决方案4】:

      与此同时,您可以采用 OOP 方法并将每个元素建模为一个对象:

      public class Example {
      
          static class Tenor {
      
              private int amount;
              private ChronoUnit unit;
              private String createdFromString;
      
              public Tenor(int amount, ChronoUnit unit, String createdFromString) {
                  this.amount = amount;
                  this.unit = unit;
                  this.createdFromString = createdFromString;
              }
      
              Duration getDuration() {
                  return unit.getDuration().multipliedBy(amount);
              }
      
              @Override
              public String toString() {
                  return createdFromString;
              }
          }
      
          static class TenorFactory {
              private static final Map<String, ChronoUnit> stringToUnitMap = new HashMap<>();
              static {
                  stringToUnitMap.put("M", ChronoUnit.MONTHS);
                  stringToUnitMap.put("Y", ChronoUnit.YEARS);
                  stringToUnitMap.put("W", ChronoUnit.WEEKS);
              }
      
              static Tenor create(String tenorString) {
                  if ("SPOT".equals(tenorString))
                      return new Tenor(0, ChronoUnit.MILLIS, tenorString);
      
                  int value = Integer.parseInt(tenorString.replaceAll("[^\\d]", ""));
                  String unitAsString = tenorString.replaceAll("[^A-Z]", "");
                  ChronoUnit unit = stringToUnitMap.get(unitAsString);
                  if (unit == null)
                      throw new IllegalArgumentException("Unknown chrono unit:" + unitAsString);
                  return new Tenor(value, unit, tenorString);
              }
          }
      
          public static void main(String[] args) {
              List<String> tenors = Arrays.asList("SPOT", "1W", "2W", "10Y", "15Y", "1M", "1Y", "20Y", "2Y", "30Y",
                      "3M", "5Y", "6M", "9M");
      
              //@formatter:off
              List<String> sortedTenors = tenors.stream().map(TenorFactory::create)
                      .sorted(Comparator.comparing(Tenor::getDuration))
                      .map(Tenor::toString)
                      .collect(Collectors.toList());
              //@formatter:on
      
              System.out.println(sortedTenors);
          }
      }
      

      如果您不了解 stream()... 行的作用,那么……您可以自己使用传统的 for 循环来完成。

      【讨论】:

        猜你喜欢
        • 2017-02-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-05-07
        • 1970-01-01
        • 2015-02-09
        相关资源
        最近更新 更多