All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squarespace.cldr.codegen.PluralCodeGenerator Maven / Gradle / Ivy

There is a newer version: 0.0.26
Show newest version
package com.squarespace.cldr.codegen;

import static com.squarespace.cldr.codegen.Types.NUMBER_OPERANDS;
import static com.squarespace.cldr.codegen.Types.PACKAGE_CLDR_PLURALS;
import static com.squarespace.cldr.codegen.Types.PLURAL_CATEGORY;
import static com.squarespace.cldr.codegen.Types.PLURAL_CONDITION;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import com.google.common.base.Splitter;
import com.squarespace.cldr.codegen.parse.PluralRulePrinter;
import com.squarespace.cldr.codegen.parse.PluralType;
import com.squarespace.cldr.codegen.reader.DataReader;
import com.squarespace.cldr.codegen.reader.PluralData;
import com.squarespace.compiler.parse.Atom;
import com.squarespace.compiler.parse.Node;
import com.squarespace.compiler.parse.Struct;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;


/**
 * Generates code to calculate plural categories using CLDR rules.
 */
public class PluralCodeGenerator {

  private static final ClassName TYPE_BASE = ClassName.get(PACKAGE_CLDR_PLURALS, "PluralRulesBase");

  public static void main(String[] args) throws Exception {
    Path outputDir = Paths.get("/Users/phensley/dev/squarespace-cldr/runtime/src/generated/java");
    new PluralCodeGenerator().generate(outputDir, DataReader.get());
  }
  
  /**
   * Generate a class for plural rule evaluation.
   *
   * This will build several Condition fields which evaluate specific AND conditions,
   * and methods which call these AND conditions joined by an OR operator.
   */
  public void generate(Path outputDir, DataReader reader) throws IOException {
    String className = "_PluralRules";
    TypeSpec.Builder type = TypeSpec.classBuilder(className)
        .addModifiers(PUBLIC)
        .superclass(TYPE_BASE);

    Map fieldMap = buildConditionFields(reader.cardinals(), reader.ordinals());
    for (Map.Entry entry : fieldMap.entrySet()) {
      type.addField(entry.getValue());
    }

    buildPluralMethod(type, "Cardinal", reader.cardinals(), fieldMap);
    buildPluralMethod(type, "Ordinal", reader.ordinals(), fieldMap);

    CodeGenerator.saveClass(outputDir, PACKAGE_CLDR_PLURALS, className, type.build());
  }

  /**
   * Creates a method to evaluate the plural rules for a specific language and plural type
   * (cardinal, ordinal).
   */
  private void buildPluralMethod(
      TypeSpec.Builder type, String pluralType, Map pluralMap, Map fieldMap) {

    MethodSpec.Builder method = MethodSpec.methodBuilder("eval" + pluralType)
        .addModifiers(PUBLIC)
        .addParameter(String.class, "language")
        .addParameter(NUMBER_OPERANDS, "o")
        .returns(PLURAL_CATEGORY);

    method.beginControlFlow("switch (language)");

    List ruleMethods = new ArrayList<>();
    for (Map.Entry entry : pluralMap.entrySet()) {
      String language = entry.getKey();
      String methodName = String.format("eval%s%s", pluralType, language.toUpperCase());
      method.addStatement("case $S:\n  return $L(o)", language, methodName);

      MethodSpec ruleMethod = buildRuleMethod(methodName, entry.getValue(), fieldMap);
      ruleMethods.add(ruleMethod);
    }

    method.addStatement("default:\n  return null");

    method.endControlFlow();
    type.addMethod(method.build());

    for (MethodSpec ruleMethod : ruleMethods) {
      type.addMethod(ruleMethod);
    }
  }

  /**
   * Builds a method that when called evaluates the rule and returns a PluralCategory.
   */
  private MethodSpec buildRuleMethod(String methodName, PluralData data, Map fieldMap) {
    MethodSpec.Builder method = MethodSpec.methodBuilder(methodName)
        .addModifiers(PRIVATE, STATIC)
        .addParameter(NUMBER_OPERANDS, "o")
        .returns(PLURAL_CATEGORY);

    for (Map.Entry entry : data.rules().entrySet()) {
      String category = entry.getKey();

      // Other is always the last condition in a set of rules.
      if (category.equals("other")) {
        // Last condition.
        method.addStatement("return PluralCategory.OTHER");
        break;
      }

      // Create a representation of the full rule for commenting.
      PluralData.Rule rule = entry.getValue();
      String ruleRepr = PluralRulePrinter.print(rule.condition);

      // Append all of the lambda methods we'll be invoking to evaluate this rule.
      List fields = new ArrayList<>();
      for (Node condition : rule.condition.asStruct().nodes()) {
        String repr = PluralRulePrinter.print(condition);
        fields.add(fieldMap.get(repr).name);
      }

      // Header comment to indicate which conditions are evaluated.
      method.addComment("  $L", ruleRepr);
      if (!Objects.equals(rule.sample, "")) {
        List samples = Splitter.on("@").splitToList(rule.sample);
        for (String sample : samples) {
          method.addComment("  $L", sample);
        }
      }

      // Emit the chain of OR conditions. If one is true we return the current category.
      int size = fields.size();
      String stmt = "if (";
      for (int i = 0; i < size; i++) {
        if (i > 0) {
          stmt += " || ";
        }
        stmt += fields.get(i) + ".eval(o)";
      }
      stmt += ")";

      // If the rule evaluates to true, return the associated plural category.
      method.beginControlFlow(stmt);
      method.addStatement("return PluralCategory." + category.toUpperCase());
      method.endControlFlow();
      method.addCode("\n");
    }

    return method.build();
  }

  /**
   * Maps an integer to each AND condition's canonical representation.
   */
  @SafeVarargs
  private final Map buildConditionFields(Map... pluralMaps) {
    Map index = new LinkedHashMap<>();
    int seq = 0;

    for (Map pluralMap : pluralMaps) {
      for (Map.Entry entry : pluralMap.entrySet()) {
        PluralData data = entry.getValue();

        // Iterate over the rules, drilling into the OR conditions and
        // building a field to evaluate each AND condition.
        for (Map.Entry rule : data.rules().entrySet()) {
          Node orCondition = rule.getValue().condition;
          if (orCondition == null) {
            continue;
          }

          // Render the representation for each AND condition, using that as a
          // key to map to the corresponding lambda Condition field that
          // computes it.
          for (Node andCondition : orCondition.asStruct().nodes()) {
            String repr = PluralRulePrinter.print(andCondition);
            if (index.containsKey(repr)) {
              continue;
            }

            // Build the field that represents the evaluation of the AND condition.
            FieldSpec field = buildConditionField(seq, andCondition.asStruct());
            index.put(repr, field);
            seq++;
          }
        }
      }
    }
    return index;
  }

  /**
   * Constructs a lambda Condition field that represents a chain of AND conditions,
   * that together is a single branch in an OR condition.
   */
  public FieldSpec buildConditionField(int index, Struct branch) {
    String fieldDoc = PluralRulePrinter.print(branch);
    String name = String.format("COND_%d", index);
    FieldSpec.Builder field = FieldSpec.builder(PLURAL_CONDITION, name, PRIVATE, STATIC, FINAL)
        .addJavadoc(fieldDoc + "\n");

    List> expressions = branch.nodes();

    CodeBlock.Builder code = CodeBlock.builder();
    code.beginControlFlow("(o) ->");

    int size = expressions.size();
    for (int i = 0; i < size; i++) {
      renderExpr(i == 0, code, expressions.get(i));
    }
    code.addStatement("return true");
    code.endControlFlow();

    field.initializer(code.build());
    return field.build();
  }

  /**
   * Render the header of a branch method.
   */
  private static void renderExpr(boolean first, CodeBlock.Builder code, Node expr) {
    Iterator> iter = expr.asStruct().nodes().iterator();

    // Parse out the two forms of operand expressions we support:
    //
    //    n = 
    //    n % m = 
    //
    Atom operand = iter.next().asAtom();
    Atom modop = null;
    Atom relop = iter.next().asAtom();
    if (relop.type() == PluralType.MODOP) {
      modop = relop;
      relop = iter.next().asAtom();
    }

    String var = (String)operand.value();
    List> rangeList = iter.next().asStruct().nodes();

    boolean decimalsZero = false;
    if (var.equals("n") && modop != null) {
      // We're applying mod to the 'n' operand, we must also ensure that
      // decimal value == 0.
      decimalsZero = true;
    }
    
    // If this is the first expression, define the variable; otherwise reuse it.
    String fmt = "zz = o.$L()";
    if (first) {
      fmt = "long " + fmt;
    }

    // Mod operations modify the operand before assignment.
    if (modop != null) {
      fmt += " % $LL";
    }

    // Emit the expression.
    if (modop != null) {
      code.addStatement(fmt, var, (int)modop.value());
    } else {
      code.addStatement(fmt, var);
    }

    renderExpr(code, rangeList, relop.value().equals("="), decimalsZero);
  }

  /**
   * Render the expression body of a branch method.
   */
  private static void renderExpr(
      CodeBlock.Builder code, List> rangeList, boolean equal, boolean decimalsZero) {
    
    int size = rangeList.size();
    String r = "";

    // Join the range list expressions with the OR operator.
    for (int i = 0; i < size; i++) {
      if (i > 0) {
        r += " || ";
      }
      r += renderRange("zz", rangeList.get(i));
    }

    // Wrap the expression in an IF.
    String fmt = "if (";
    if (decimalsZero) {
      fmt += "o.nd() != 0 || ";
    }

    if (equal) {
      fmt += size == 1 ? "!$L" : "!($L)";
    } else {
      fmt += "($L)";
    }

    fmt += ")";

    // Wrap the IF as a block.
    code.beginControlFlow(fmt, r);
    code.addStatement("return false");
    code.endControlFlow();
  }

  /**
   * Render a the range segment of an expression.
   */
  private static String renderRange(String name, Node node) {
    if (node.type() == PluralType.RANGE) {
      Struct range = node.asStruct();
      int start = (Integer) range.nodes().get(0).asAtom().value();
      int end = (Integer) range.nodes().get(1).asAtom().value();
        return String.format("(%s >= %d && %s <= %d)", name, start, name, end);
    }
    return String.format("(%s == %s)", name, node.asAtom().value());
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy