Unit Conversion and Display String Formatting

AU went very well for me, and I think this was the one I liked most of all so far, to my own surprise. Now I am already at the next conference in Moscow, from where I continue to Tel Aviv tomorrow. December is always my monster travelling month, and I never get to prepare for Christmas or enjoy the dark and cosy celebration of Advent. But I really did have fun and enjoy AU in Las Vegas.

Sunday morning my colleague Marat Mirgaleev invited me to join him in his weekly volleyball game, which was a wonderful break from the conference presentation preparation. Marat is also a member of the ADN DevTech team and spelled Марат Миргалеев in Cyrillic. Another Autodesk colleague who also joined was Rustam Ibragimov, Рустам Ибрагимов. Here are Rustam, Marat, I and our all-time star, the volleyball herself:

Rustam, Marat, Jeremy and the volleyball

Here we are now in the Autodesk Moscow office:

DevDay conference in Moscow

Fittingly enough, here is a question from Russia, by Victor Chekalin, or Виктор Чекалин in Cyrillic, on formatting a floating point number as a display string using the current project units. This issue has cropped up several times recently, and various solutions based on the same principle have been suggested, among others by Joe Offord of Enclos, who already shared insights on accessing curtain wall geometry, speeding up the interactive selection process, mirroring in a new family and changing the active view, and constructing a planar face transform.

All of the solutions to this problem I have seen revolve around stuffing in the value to format into an unused parameter picked up from some arbitrary database element and then calling the AsValueString method on it. The Parameter class provides this functionality, but unfortunately the API does not include any stand-alone access to it. Here is Victor's initial query:

Question: I need to convert a numeric value to a corresponding display string honouring the current Revit ProjectUnit setting. I cannot find how to do it in the Revit help and began search the answer in your amazing site. I found the Unit Conversion tool and thought it would fulfil my need, but I was wrong :(

Looking at the Unit Converter code, I discovered that retrieving scale factor to internal units is not easy and you used some trick to get it: you change ProjectUnit, write "1" to family parameter, read value from this parameter, change ProjectUnit back. It works but is really hard and is not an official way.

Answer: Try something like this on some otherwise unused length parameter 'p':

  Dim value As String = "=2' + 4'/3"
  Dim t As New Transaction(doc, "Format Length")
  t.Start()
  p.SetValueString(value)
  value = p.AsValueString
  t.RollBack()
  Return value

In fact, Joe provided the following helper methods based on this idea implemented in VB:

All three of these methods create and then roll back a temporary transaction to perform their task.

Here is the full implementation of the first of these, StringValueString:

  Public Shared Function StringValueString( _
    ByVal doc As Document, _
    ByVal value As String) As String
 
    ' Locate the arbitrary Length parameter
 
    Dim p As Parameter _
      = doc.ProjectInformation.Parameter( _
      "Parameter Name")
 
    If p Is Nothing Then
      TaskDialog.Show( _
        "Revit", _
        "Missing Project Parameter: Parameter Name")
      Return value
    End If
 
    Dim tr As New Transaction(doc)
    tr.Start("Format Length")
 
    Try
      p.SetValueString(value)
      value = p.AsValueString
    Catch ex As Exception
 
    End Try
 
    tr.RollBack()
 
    Return value
 
  End Function

For the second, DblValueString, simply replace the input argument by a floating-point value 'ByVal value As Double' and the two lines to perform the actual conversion by

    p.Set(value)
    sValueString = p.AsValueString

Finally, for the third, ValueStringDbl, the input argument 'value' is again a string and the conversion to the floating-point return value is performed by

    p.SetValueString(value)
    dValue = p.AsDouble

Response: Thanks for the answer.

I wrote some simple extension methods for the Revit API Parameter class to get value in project units and in meters value. Now it works with Length, Volume and Area (now I don't need any more). It would take much time to add support for all unit conversions.

Here is the entire implementation of my ParameterUnitConverter class. It defines the following methods and data:

Here is the complete implementation of this:

public static class ParameterUnitConverter
{
  private const double METERS_IN_FEET = 0.3048;
 
  public static double AsProjectUnitTypeDouble(
    this Parameter param )
  {
    if( param.StorageType != StorageType.Double )
      throw new NotSupportedException(
        "Parameter does not have double value" );
 
    double imperialValue = param.AsDouble();
 
    Document document = param.Element.Document;
 
    UnitType ut = ConvertParameterTypeToUnitType(
      param.Definition.ParameterType );
 
    FormatOptions fo = document.ProjectUnit
      .get_FormatOptions( ut );
 
    DisplayUnitType dut = fo.Units;
 
    // Unit Converter
    // http://www.asknumbers.com
 
    switch( dut )
    {
      #region Length
 
      case DisplayUnitType.DUT_METERS:
        return imperialValue * METERS_IN_FEET; //feet
      case DisplayUnitType.DUT_CENTIMETERS:
        return imperialValue * METERS_IN_FEET * 100;
      case DisplayUnitType.DUT_DECIMAL_FEET:
        return imperialValue;
      case DisplayUnitType.DUT_DECIMAL_INCHES:
        return imperialValue * 12;
      case DisplayUnitType.DUT_FEET_FRACTIONAL_INCHES:
        NotSupported( dut );
        break;
      case DisplayUnitType.DUT_FRACTIONAL_INCHES:
        NotSupported( dut );
        break;
      case DisplayUnitType.DUT_METERS_CENTIMETERS:
        return imperialValue * METERS_IN_FEET; //feet
      case DisplayUnitType.DUT_MILLIMETERS:
        return imperialValue * METERS_IN_FEET * 1000;
 
      #endregion // Length
 
      #region Area
 
      case DisplayUnitType.DUT_SQUARE_FEET:
        return imperialValue;
      case DisplayUnitType.DUT_ACRES:
        return imperialValue * 1 / 43560.039;
      case DisplayUnitType.DUT_HECTARES:
        return imperialValue * 1 / 107639.104;
      case DisplayUnitType.DUT_SQUARE_CENTIMETERS:
        return imperialValue * Math.Pow( METERS_IN_FEET * 100, 2 );
      case DisplayUnitType.DUT_SQUARE_INCHES:
        return imperialValue * Math.Pow( 12, 2 );
      case DisplayUnitType.DUT_SQUARE_METERS:
        return imperialValue * Math.Pow( METERS_IN_FEET, 2 );
      case DisplayUnitType.DUT_SQUARE_MILLIMETERS:
        return imperialValue * Math.Pow( METERS_IN_FEET * 1000, 2 );
 
      #endregion // Area
 
      #region Volume
      case DisplayUnitType.DUT_CUBIC_FEET:
        return imperialValue;
      case DisplayUnitType.DUT_CUBIC_CENTIMETERS:
        return imperialValue * Math.Pow( METERS_IN_FEET * 100, 3 );
      case DisplayUnitType.DUT_CUBIC_INCHES:
        return imperialValue * Math.Pow( 12, 3 );
      case DisplayUnitType.DUT_CUBIC_METERS:
        return imperialValue * Math.Pow( METERS_IN_FEET, 3 );
      case DisplayUnitType.DUT_CUBIC_MILLIMETERS:
        return imperialValue * Math.Pow( METERS_IN_FEET * 1000, 3 );
      case DisplayUnitType.DUT_CUBIC_YARDS:
        return imperialValue * 1 / Math.Pow( 3, 3 );
      case DisplayUnitType.DUT_GALLONS_US:
        return imperialValue * 7.5;
      case DisplayUnitType.DUT_LITERS:
        return imperialValue * 28.31684;
 
      #endregion // Volume
 
      default:
        NotSupported( dut );
        break;
    }
 
    throw new NotSupportedException();
  }

  public static double AsMetersValue(
    this Parameter param )
  {
    if( param.StorageType != StorageType.Double )
      throw new NotSupportedException(
        "Parameter does not have double value" );
 
    double imperialValue = param.AsDouble();
 
    UnitType ut = ConvertParameterTypeToUnitType(
      param.Definition.ParameterType );
 
    switch( ut )
    {
      case UnitType.UT_Length:
        return imperialValue * METERS_IN_FEET;
 
      case UnitType.UT_Area:
        return imperialValue * Math.Pow(
          METERS_IN_FEET, 2 );
 
      case UnitType.UT_Volume:
        return imperialValue * Math.Pow(
          METERS_IN_FEET, 3 );
    }
    throw new NotSupportedException();
  }
 
  public static UnitType
    ConvertParameterTypeToUnitType(
      ParameterType parameterType )
  {
    if( _map_parameter_type_to_unit_type.ContainsKey(
      parameterType ) )
    {
      return _map_parameter_type_to_unit_type[
        parameterType];
    }
    else
    {
      // If we made it this far, there's 
      // no entry in the dictionary.
 
      throw new ArgumentException(
        "Cannot convert ParameterType '"
          + parameterType.ToString()
          + "' to a UnitType." );
    }
  }
 
  static Dictionary<ParameterType, UnitType>
    _map_parameter_type_to_unit_type
      = new Dictionary<ParameterType, UnitType>()
  {
    // This data could come from a file, 
    // or (better yet) from Revit itself...
 
    {ParameterType.Angle, UnitType.UT_Angle},
    {ParameterType.Area, UnitType.UT_Area},
    {ParameterType.AreaForce, UnitType.UT_AreaForce},
    {ParameterType.AreaForcePerLength, UnitType.UT_AreaForcePerLength},
    //map.Add(UnitType.UT_AreaForceScale, ParameterType.???);
    {ParameterType.ColorTemperature, UnitType.UT_Color_Temperature},
    {ParameterType.Currency, UnitType.UT_Currency},
    //map.Add(UnitType.UT_DecSheetLength, ParameterType.???);
    {ParameterType.ElectricalApparentPower, UnitType.UT_Electrical_Apparent_Power},
    {ParameterType.ElectricalCurrent, UnitType.UT_Electrical_Current},
    {ParameterType.ElectricalEfficacy, UnitType.UT_Electrical_Efficacy},
    {ParameterType.ElectricalFrequency, UnitType.UT_Electrical_Frequency},
    {ParameterType.ElectricalIlluminance, UnitType.UT_Electrical_Illuminance},
    {ParameterType.ElectricalLuminance, UnitType.UT_Electrical_Luminance},
    {ParameterType.ElectricalLuminousFlux, UnitType.UT_Electrical_Luminous_Flux},
    {ParameterType.ElectricalLuminousIntensity, UnitType.UT_Electrical_Luminous_Intensity},
    {ParameterType.ElectricalPotential, UnitType.UT_Electrical_Potential},
    {ParameterType.ElectricalPower, UnitType.UT_Electrical_Power},
    {ParameterType.ElectricalPowerDensity, UnitType.UT_Electrical_Power_Density},
    {ParameterType.ElectricalWattage, UnitType.UT_Electrical_Wattage},
    {ParameterType.Force, UnitType.UT_Force},
    {ParameterType.ForceLengthPerAngle, UnitType.UT_ForceLengthPerAngle},
    {ParameterType.ForcePerLength, UnitType.UT_ForcePerLength},
    //map.Add(UnitType.UT_ForceScale, ParameterType.???);
    {ParameterType.HVACAirflow, UnitType.UT_HVAC_Airflow},
    {ParameterType.HVACAirflowDensity, UnitType.UT_HVAC_Airflow_Density},
    {ParameterType.HVACAirflowDividedByCoolingLoad, UnitType.UT_HVAC_Airflow_Divided_By_Cooling_Load},
    {ParameterType.HVACAirflowDividedByVolume, UnitType.UT_HVAC_Airflow_Divided_By_Volume},
    {ParameterType.HVACAreaDividedByCoolingLoad, UnitType.UT_HVAC_Area_Divided_By_Cooling_Load},
    {ParameterType.HVACAreaDividedByHeatingLoad, UnitType.UT_HVAC_Area_Divided_By_Heating_Load},
    {ParameterType.HVACCoefficientOfHeatTransfer, UnitType.UT_HVAC_CoefficientOfHeatTransfer},
    {ParameterType.HVACCoolingLoad, UnitType.UT_HVAC_Cooling_Load},
    {ParameterType.HVACCoolingLoadDividedByArea, UnitType.UT_HVAC_Cooling_Load_Divided_By_Area},
    {ParameterType.HVACCoolingLoadDividedByVolume, UnitType.UT_HVAC_Cooling_Load_Divided_By_Volume},
    {ParameterType.HVACCrossSection, UnitType.UT_HVAC_CrossSection},
    {ParameterType.HVACDensity, UnitType.UT_HVAC_Density},
    {ParameterType.HVACDuctSize, UnitType.UT_HVAC_DuctSize},
    {ParameterType.HVACEnergy, UnitType.UT_HVAC_Energy},
    {ParameterType.HVACFactor, UnitType.UT_HVAC_Factor},
    {ParameterType.HVACFriction, UnitType.UT_HVAC_Friction},
    {ParameterType.HVACHeatGain, UnitType.UT_HVAC_HeatGain},
    {ParameterType.HVACHeatingLoad, UnitType.UT_HVAC_Heating_Load},
    {ParameterType.HVACHeatingLoadDividedByArea, UnitType.UT_HVAC_Heating_Load_Divided_By_Area},
    {ParameterType.HVACHeatingLoadDividedByVolume, UnitType.UT_HVAC_Heating_Load_Divided_By_Volume},
    {ParameterType.HVACPower, UnitType.UT_HVAC_Power},
    {ParameterType.HVACPowerDensity, UnitType.UT_HVAC_Power_Density},
    {ParameterType.HVACPressure, UnitType.UT_HVAC_Pressure},
    {ParameterType.HVACRoughness, UnitType.UT_HVAC_Roughness},
    {ParameterType.HVACSlope, UnitType.UT_HVAC_Slope},
    {ParameterType.HVACTemperature, UnitType.UT_HVAC_Temperature},
    {ParameterType.HVACVelocity, UnitType.UT_HVAC_Velocity},
    {ParameterType.HVACViscosity, UnitType.UT_HVAC_Viscosity},
    {ParameterType.Length, UnitType.UT_Length},
    {ParameterType.LinearForce, UnitType.UT_LinearForce},
    {ParameterType.LinearForceLengthPerAngle, UnitType.UT_LinearForceLengthPerAngle},
    {ParameterType.LinearForcePerLength, UnitType.UT_LinearForcePerLength},
    // map.Add(UnitType.UT_LinearForceScale, ParameterType.???);
    {ParameterType.LinearMoment, UnitType.UT_LinearMoment},
    // map.Add(UnitType.UT_LinearMomentScale, ParameterType.???);
    {ParameterType.Moment, UnitType.UT_Moment},
    ///map.Add(UnitType.UT_MomentScale, ParameterType.???);
    {ParameterType.Number, UnitType.UT_Number},
    {ParameterType.PipeSize, UnitType.UT_PipeSize},
    {ParameterType.PipingDensity, UnitType.UT_Piping_Density},
    {ParameterType.PipingFlow, UnitType.UT_Piping_Flow},
    {ParameterType.PipingFriction, UnitType.UT_Piping_Friction},
    {ParameterType.PipingPressure, UnitType.UT_Piping_Pressure},
    {ParameterType.PipingRoughness, UnitType.UT_Piping_Roughness},
    {ParameterType.PipingSlope, UnitType.UT_Piping_Slope},
    {ParameterType.PipingTemperature, UnitType.UT_Piping_Temperature},
    {ParameterType.PipingVelocity, UnitType.UT_Piping_Velocity},
    {ParameterType.PipingViscosity, UnitType.UT_Piping_Viscosity},
    {ParameterType.PipingVolume, UnitType.UT_Piping_Volume},
    //map.Add(UnitType.UT_SheetLength, ParameterType.???);
    //map.Add(UnitType.UT_SiteAngle, ParameterType.???);
    {ParameterType.Slope, UnitType.UT_Slope},
    {ParameterType.Stress, UnitType.UT_Stress},
    {ParameterType.TemperalExp, UnitType.UT_TemperalExp},
    {ParameterType.UnitWeight, UnitType.UT_UnitWeight},
    {ParameterType.Volume, UnitType.UT_Volume},
    {ParameterType.WireSize, UnitType.UT_WireSize},
  };
}

I used some functions from the Unit converter. Here is an external command sample to test it by iterating over and applying it to all floating point valued parameters on a selected element.

Hope you'll find this useful.

Many thanks to Joe and Victor for putting together and sharing these nice solutions!

I added the ParameterUnitConverter class to The Building Coder samples and defined a new external command CmdParameterUnitConverter based on Victor's code to test it. Here is version 2012.0.96.0 of The Building Coder samples including the new utility class and command.

This is the output generated by the command in the Visual Studio debug output window on selecting a wall element in the rac_basic_sample_project.rvt sample model:

Parameter name: Top Extension Distance
  Parameter value (imperial): 0
  Parameter unit value: 0
  Parameter AsValueString: 0.0

Parameter name: Length
  Parameter value (imperial): 45.5
  Parameter unit value: 13868.4
  Parameter AsValueString: 13868.4

Parameter name: Base Extension Distance
  Parameter value (imperial): 0
  Parameter unit value: 0
  Parameter AsValueString: 0.0

Parameter name: Top Offset
  Parameter value (imperial): 0
  Parameter unit value: 0
  Parameter AsValueString: 0.0

Parameter name: Volume
  Parameter value (imperial): 455.9586023831
  Parameter unit value: 12.911309795985
  Parameter AsValueString: 12.911 m³

Parameter name: Unconnected Height
  Parameter value (imperial): 18.0446194225722
  Parameter unit value: 5500
  Parameter AsValueString: 5500.0

Parameter name: Base Offset
  Parameter value (imperial): 0
  Parameter unit value: 0
  Parameter AsValueString: 0.0

Parameter name: Area
  Parameter value (imperial): 694.880910031848
  Parameter unit value: 64.5565489799251
  Parameter AsValueString: 64.557 m²

Here, the parameter value labelled 'imperial' is the internal Revit database unit, e.g. feet for length. Please note that not all internal database units are imperial. In fact, only length is measured in feet, and thus also area and volume. Other internal units are SI-based.