cancel
Showing results for 
Search instead for 
Did you mean: 
cancel
Showing results for 
Search instead for 
Did you mean: 

Community Tip - You can subscribe to a forum, label or individual post and receive email notifications when someone posts a new topic or reply. Learn more! X

Translate the entire conversation x

Converting fractions to decimals in attribute entry

avillanueva
23-Emerald I

Converting fractions to decimals in attribute entry

Related to this idea request: https://community.ptc.com/t5/Windchill-Ideas/Support-conversion-of-fractions-to-decimals-for-IBA-real-number/idi-p/1039253

I started poking in to see how this could be done. I noticed that attributes with real numbers with units have a JS validation script associated onBlur. The function is "jsca.columns.numericRenderer.validateFloatingPointWithUnit" but surprisingly, this function is defined in 5 different locations in the codebase. Not sure which one is the one called when the attribute entry is validated but it should not be too hard to narrow that down.

avillanueva_0-1761328076954.png

Digging further, this function simply takes in a regular expression and ensures that the entry matches it:

avillanueva_1-1761328156061.png

Pumping this into AI let's us know what its checking:

avillanueva_2-1761328209552.png

The regular expression [^0-9\-\,\.E] matches any single character that is not a digit, a hyphen, a comma, a period, or the uppercase letter E. 
Here is a breakdown of the expression:
[]: This is a character class, which specifies a set of characters to match. It matches any one character contained within the brackets.
^: Placed at the very beginning of a character class, this symbol negates the set. It means to match any character not in the specified list.
0-9: This is a range that matches any single digit from 0 to 9.
\-: A hyphen (-) inside a character class can indicate a range, but since it appears before or after other characters, it loses its special meaning and is treated as a literal hyphen. However, escaping it with a backslash (\) explicitly ensures it is treated as a literal character.
\,: The comma is a literal character. Escaping it with a backslash is unnecessary but harmless. It matches a single comma.
\.: A period (.) normally matches any character, but inside a character class, it is treated as a literal period. The backslash escapes it, but it still means "a literal period".
E: The uppercase letter E is a literal character and matches only the letter E. 

Actually, the function looks at the value before the first space character if it exists and ensures that the first part matches a valid number.  Simple. 

My thought would be to hook in another JS call before all this to transform the input if it encounted a "/" character in that first part, converting it to a decimal. What do you think? I can call it only if it detects a "/" otherwise pass through. Still strange there is 5 copies of this function floating around...

ACCEPTED SOLUTION

Accepted Solutions

Ok, I ended up taking this path. I modified the numericJSRenderer.jsfrag file instead of putting my stuff in a custom.jsfrag files. Why? Well, I had to make changes to jsca.columns.numericRenderer.validateFloatingPointWithUnit() to have it call my method first so figured it was one less file to manage. So, basically if we detect a single /, send it to my fuction to convert it back to decimal. If it ends up detecting bad input, it returns null, and normal error message. If not, modify the value and also update the input field to show decimals. Seems to work great up need to test a bit more. Here is code. If you spot a bug or edge case, let me know.

/**
 *Validates the given numeric input element value to check if its number or its number plus unit.
 *@private
 */
jsca.columns.numericRenderer.validateFloatingPointWithUnit = function(elem, errorMsgRB, regEx){
    var value = elem.value;
    value = value.trim();
    //test if fraction entered
    var fracArr = value.split("/");
    if (fracArr.length === 2) {
      //user entered a normal fractional value
      var transformedValue=jsca.columns.numericRenderer.convertFractionToFloatingPoint(value);
      if (!transformedValue) {
        JCAAlert(errorMsgRB);
      } else {
        value=transformedValue;
        elem.value= transformedValue;
      }
    } else if (fracArr.length > 2) {
      //user entered 1/3/2 m which is invalid
      JCAAlert(errorMsgRB);
    } 
    var strArr = value.split(" ");
    if (strArr.length === 1) { 
      //user entered just the number, no unit.
      PTC.wizard.validateInputValue(elem, value, regEx, errorMsgRB, []);
    } else if (strArr.length === 2){ 
      //user entered 10.32 m, validates just the first part. Units is validated by type layer on form submission.
      PTC.wizard.validateInputValue(elem, strArr[0], regEx, "com.ptc.core.ui.errorMessagesRB.NUMERIC_VALUE_ERROR_MSG", []);
    } else if (strArr.length > 2){ 
      //user entered 10.32 m mm which is invalid
      JCAAlert(errorMsgRB);
    }
};

/**
 *Takes a whole, faction or mixed fraction and converts to floating point decimal value
 *Called from within jsca.columns.numericRenderer.validateFloatingPointWithUnit as a hook
 *Assumed value must contain a / which was validated in calling function
 *@private
 */
jsca.columns.numericRenderer.convertFractionToFloatingPoint = function(value){
  const isNegative = value.startsWith('-');
  // Remove the negative sign and any trailing non-numeric units
  const cleanInput = (isNegative ? value.substring(1) : value).split(/\s+/);
  let wholeNumber = 0;
  let fractionPart = "0/1";
  let unit = "";
  // 2. Parse numeric parts (Whole number, Fraction, or Decimal)
  if (cleanInput.length === 3) {
    // Mixed fraction with unit: ["1", "1/2", "m"]
    wholeNumber = parseFloat(cleanInput[0]);
    fractionPart = cleanInput[1];
    unit = cleanInput[2];
  } else if (cleanInput.length === 2) { //fraction with unit or mixed case without
    // Simple fraction with unit: ["3/4","m"] or mixed fraction: ["1","3/4"]
    if (cleanInput[0].includes('/')) {
    	fractionPart = cleanInput[0];
    	unit=cleanInput[1];
    } else { //mixed fraction with no unit
    	wholeNumber = parseFloat(cleanInput[0]);
    	fractionPart = cleanInput[1];
    }
  } else if (cleanInput.length === 1) {
    // Simple fraction: ["3/4"]
    	fractionPart = cleanInput[0];
  } else { //not a case we are handling
      return null;
  }

  // 3. Process the fraction part
  const [num, den] = fractionPart.split('/').map(Number);
  if (isNaN(num) || isNaN(den) || den === 0) return null;

  // 4. Combine magnitude and re-apply sign
  const magnitude = wholeNumber + (num / den);
  const finalValue = isNegative ? -magnitude : magnitude;

  // 5. Round to max 6 decimal places
  // toFixed(6) handles repeating decimals by rounding [MDN Web Docs]
  return parseFloat(finalValue.toFixed(6))+" " + unit;
};

Here is an animation of it working.

View solution in original post

3 REPLIES 3

https://www.ptc.com/en/support/article/CS71276?source=search

Article above provides a method for customization. Using this, netmarkets\javascript\util\jsfrags\attribute\numericJSRenderer.jsfrag is the customization point but you also need to compile this according to the link above to update windchill-all.js which is what is referenced in the page. Easy enough for AI to handle coding but here are the cases we need to consider:

  1. Normal numbers: 5 = 5
  2. Normal fractions: 1/4 = 0.25
  3. Mixed number: 1 1/4= 1.25
  4. Mixed numbers alternate: 1-1/4=1.25
  5. Negative fractions: -1/8=-0.125
  6. Repeating decimals: 1/3=0.3333 (stop at some point, round)
  7. Divide by 0: 5/0 = NaN

We should be able to insert a method call to this function which converts it, update the element and continues on to validation. I will post when complete for all.

Ok, I ended up taking this path. I modified the numericJSRenderer.jsfrag file instead of putting my stuff in a custom.jsfrag files. Why? Well, I had to make changes to jsca.columns.numericRenderer.validateFloatingPointWithUnit() to have it call my method first so figured it was one less file to manage. So, basically if we detect a single /, send it to my fuction to convert it back to decimal. If it ends up detecting bad input, it returns null, and normal error message. If not, modify the value and also update the input field to show decimals. Seems to work great up need to test a bit more. Here is code. If you spot a bug or edge case, let me know.

/**
 *Validates the given numeric input element value to check if its number or its number plus unit.
 *@private
 */
jsca.columns.numericRenderer.validateFloatingPointWithUnit = function(elem, errorMsgRB, regEx){
    var value = elem.value;
    value = value.trim();
    //test if fraction entered
    var fracArr = value.split("/");
    if (fracArr.length === 2) {
      //user entered a normal fractional value
      var transformedValue=jsca.columns.numericRenderer.convertFractionToFloatingPoint(value);
      if (!transformedValue) {
        JCAAlert(errorMsgRB);
      } else {
        value=transformedValue;
        elem.value= transformedValue;
      }
    } else if (fracArr.length > 2) {
      //user entered 1/3/2 m which is invalid
      JCAAlert(errorMsgRB);
    } 
    var strArr = value.split(" ");
    if (strArr.length === 1) { 
      //user entered just the number, no unit.
      PTC.wizard.validateInputValue(elem, value, regEx, errorMsgRB, []);
    } else if (strArr.length === 2){ 
      //user entered 10.32 m, validates just the first part. Units is validated by type layer on form submission.
      PTC.wizard.validateInputValue(elem, strArr[0], regEx, "com.ptc.core.ui.errorMessagesRB.NUMERIC_VALUE_ERROR_MSG", []);
    } else if (strArr.length > 2){ 
      //user entered 10.32 m mm which is invalid
      JCAAlert(errorMsgRB);
    }
};

/**
 *Takes a whole, faction or mixed fraction and converts to floating point decimal value
 *Called from within jsca.columns.numericRenderer.validateFloatingPointWithUnit as a hook
 *Assumed value must contain a / which was validated in calling function
 *@private
 */
jsca.columns.numericRenderer.convertFractionToFloatingPoint = function(value){
  const isNegative = value.startsWith('-');
  // Remove the negative sign and any trailing non-numeric units
  const cleanInput = (isNegative ? value.substring(1) : value).split(/\s+/);
  let wholeNumber = 0;
  let fractionPart = "0/1";
  let unit = "";
  // 2. Parse numeric parts (Whole number, Fraction, or Decimal)
  if (cleanInput.length === 3) {
    // Mixed fraction with unit: ["1", "1/2", "m"]
    wholeNumber = parseFloat(cleanInput[0]);
    fractionPart = cleanInput[1];
    unit = cleanInput[2];
  } else if (cleanInput.length === 2) { //fraction with unit or mixed case without
    // Simple fraction with unit: ["3/4","m"] or mixed fraction: ["1","3/4"]
    if (cleanInput[0].includes('/')) {
    	fractionPart = cleanInput[0];
    	unit=cleanInput[1];
    } else { //mixed fraction with no unit
    	wholeNumber = parseFloat(cleanInput[0]);
    	fractionPart = cleanInput[1];
    }
  } else if (cleanInput.length === 1) {
    // Simple fraction: ["3/4"]
    	fractionPart = cleanInput[0];
  } else { //not a case we are handling
      return null;
  }

  // 3. Process the fraction part
  const [num, den] = fractionPart.split('/').map(Number);
  if (isNaN(num) || isNaN(den) || den === 0) return null;

  // 4. Combine magnitude and re-apply sign
  const magnitude = wholeNumber + (num / den);
  const finalValue = isNegative ? -magnitude : magnitude;

  // 5. Round to max 6 decimal places
  // toFixed(6) handles repeating decimals by rounding [MDN Web Docs]
  return parseFloat(finalValue.toFixed(6))+" " + unit;
};

Here is an animation of it working.

I did not write support for #4 but validated with these test cases:

console.log(convertFractionToFloatingPoint("-1/2"));
console.log(convertFractionToFloatingPoint("9/0"));
console.log(convertFractionToFloatingPoint("1 1/4"));
console.log(convertFractionToFloatingPoint("1 1/0"));
console.log(convertFractionToFloatingPoint("6/3"));
console.log(convertFractionToFloatingPoint("1/3"));
console.log(convertFractionToFloatingPoint("2/7"));
console.log(convertFractionToFloatingPoint("1/64"));
console.log(convertFractionToFloatingPoint("1/8 in"));
console.log(convertFractionToFloatingPoint("1 3/8 k"));
console.log(convertFractionToFloatingPoint("-1/6"));
console.log(convertFractionToFloatingPoint("-1.3/6"));
console.log(convertFractionToFloatingPoint("-2 1/6"));
console.log(convertFractionToFloatingPoint("-2 1/4.2"));
console.log(convertFractionToFloatingPoint("-2 1/6 m"));
console.log(convertFractionToFloatingPoint("IN/Valid m"));
console.log(convertFractionToFloatingPoint("IN/Valid"));

-0.5
null
1.25
null
2
0.333333
0.285714
0.015625
0.125 in
1.375 k
-0.166667
-0.216667
-2.166667
-2.238095
-2.166667 m
null
null
null

Announcements
Top Tags