2009-05-11 26 views
16

Hay una clase de diferencias muy fresco alojado por Google aquí:¿Cómo puedo usar JavaScript dentro de una macro de Excel?

http://code.google.com/p/google-diff-match-patch/

lo he usado antes en algunos sitios web, pero ahora tengo que usarlo dentro una macro de Excel para comparar el texto entre dos celdas.

Sin embargo, solo está disponible en JavaScript, Python, Java y C++, no en VBA.

Mis usuarios están limitados a Excel 2003, por lo que una solución .NET pura no funcionaría. Traducir el código a VBA manualmente tomaría demasiado tiempo y dificultaría la actualización.

Una opción que consideré era compilar el código fuente JavaScript o Java utilizando los compiladores .NET (JScript.NET o J #), usar Reflector para dar salida como VB.NET, y finalmente degradar el código VB.NET manualmente a VBA, dándome una solución pura de VBA. Después de tener problemas para compilar con cualquier compilador .NET, abandoné esta ruta.

Asumiendo que podría haber obtenido una biblioteca .NET en funcionamiento, también podría haber usado ExcelDna (http://www.codeplex.com/exceldna), un complemento de código abierto de Excel para facilitar la integración del código .NET.

Mi última idea fue alojar un objeto de Internet Explorer, enviarle el código fuente de JavaScript y llamarlo. Incluso si hiciera que esto funcionara, supongo que sería muy lento y desordenado.

ACTUALIZACIÓN: ¡Solución encontrada!

Utilicé el método WSC que se describe a continuación con la respuesta aceptada. Tenía que cambiar el código de WSC un poco para limpiar los diferenciales y me devuelva una matriz compatible con VBA de matrices:

function DiffFast(text1, text2) 
{ 
    var d = dmp.diff_main(text1, text2, true); 
    dmp.diff_cleanupSemantic(d); 
    var dictionary = new ActiveXObject("Scripting.Dictionary"); // VBA-compatible array 
    for (var i = 0; i < d.length; i++) { 
    dictionary.add(i, JS2VBArray(d[i])); 
    } 
    return dictionary.Items(); 
} 

function JS2VBArray(objJSArray) 
{ 
    var dictionary = new ActiveXObject("Scripting.Dictionary"); 
    for (var i = 0; i < objJSArray.length; i++) { 
     dictionary.add(i, objJSArray[ i ]); 
     } 
    return dictionary.Items(); 
} 

que registró el CSM y funcionó muy bien. El código en VBA para llamar es de la siguiente manera:

Public Function GetDiffs(ByVal s1 As String, ByVal s2 As String) As Variant() 
    Dim objWMIService As Object 
    Dim objDiff As Object 
    Set objWMIService = GetObject("winmgmts:") 
    Set objDiff = CreateObject("Google.DiffMatchPath.WSC") 
    GetDiffs = objDiff.DiffFast(s1, s2) 
    Set objDiff = Nothing 
    Set objWMIService = Nothing 
End Function 

(I intentado mantener una única objWMIService global y objDiff alrededor, así que no tendría que crear/destruir estos para cada celda, pero no parecía para hacer una diferencia en el rendimiento.)

Luego escribí mi macro principal. Toma tres parámetros: un rango (una columna) de valores originales, un rango de valores nuevos y un rango donde el diff debe volcar los resultados. Todos son asumidos para tener la misma cantidad de filas, no tengo ninguna verificación de errores importante aquí.

Public Sub DiffAndFormat(ByRef OriginalRange As Range, ByRef NewRange As Range, ByRef DeltaRange As Range) 
    Dim idiff As Long 
    Dim thisDiff() As Variant 
    Dim diffop As String 
    Dim difftext As String 
    difftext = "" 
    Dim diffs() As Variant 
    Dim OriginalValue As String 
    Dim NewValue As String 
    Dim DeltaCell As Range 
    Dim row As Integer 
    Dim CalcMode As Integer 

Estas tres líneas siguientes acelerar la actualización sin estropear modo de cálculo preferido del usuario después:

Application.ScreenUpdating = False 
    CalcMode = Application.Calculation 
    Application.Calculation = xlCalculationManual 
    For row = 1 To OriginalRange.Rows.Count 
     difftext = "" 
     OriginalValue = OriginalRange.Cells(row, 1).Value 
     NewValue = NewRange.Cells(row, 1).Value 
     Set DeltaCell = DeltaRange.Cells(row, 1) 
     If OriginalValue = "" And NewValue = "" Then 

Borrado de los diferenciales anteriores, en su caso, es importante:

  Erase diffs 

Este la prueba es un atajo visual para mis usuarios por lo que está claro cuando no hay ningún cambio:

 ElseIf OriginalValue = NewValue Then 
      difftext = "No change." 
      Erase diffs 
     Else 

Combine todo el texto en conjunto como el valor de la celda delta, si el texto era idéntico, insertado o eliminado:

  diffs = GetDiffs(OriginalValue, NewValue) 
      For idiff = 0 To UBound(diffs) 
       thisDiff = diffs(idiff) 
       difftext = difftext & thisDiff(1) 
      Next 
     End If 

Usted tiene que fijar el valor antes iniciar el formateo:

 DeltaCell.value2 = difftext 
     Call FormatDiff(diffs, DeltaCell) 
    Next 
    Application.ScreenUpdating = True 
    Application.Calculation = CalcMode 
End Sub 

Aquí está el código que interpreta los diferenciales y formatea la célula delta:

Public Sub FormatDiff(ByRef diffs() As Variant, ByVal cell As Range) 
    Dim idiff As Long 
    Dim thisDiff() As Variant 
    Dim diffop As String 
    Dim difftext As String 
    cell.Font.Strikethrough = False 
    cell.Font.ColorIndex = 0 
    cell.Font.Bold = False 
    If Not diffs Then Exit Sub 
    Dim lastlen As Long 
    Dim thislen As Long 
    lastlen = 1 
    For idiff = 0 To UBound(diffs) 
     thisDiff = diffs(idiff) 
     diffop = thisDiff(0) 
     thislen = Len(thisDiff(1)) 
     Select Case diffop 
      Case -1 
       cell.Characters(lastlen, thislen).Font.Strikethrough = True 
       cell.Characters(lastlen, thislen).Font.ColorIndex = 16 ' Dark Gray http://www.microsoft.com/technet/scriptcenter/resources/officetips/mar05/tips0329.mspx 
      Case 1 
       cell.Characters(lastlen, thislen).Font.Bold = True 
       cell.Characters(lastlen, thislen).Font.ColorIndex = 32 ' Blue 
     End Select 
     lastlen = lastlen + thislen 
    Next 
End Sub 

Hay algunas oportunidades para la optimización, pero hasta ahora está funcionando bien. Gracias a todos los que ayudaron!

+0

genial. Gustoso de trabajar para ti. En el futuro, si lo desea, puede responder su propia pregunta. Aparecerá en un cuadro de texto azul; visualmente está claro que lo has publicado. – Cheeso

+0

El proyecto diff/merge/patch de Google ahora incluye un puerto C# (totalmente administrado). –

Respuesta

11

El enfoque más simple puede ser insertar la lógica Javascript diff en un componente COM directamente utilizando Javascript. Esto es posible a través de algo llamado "Windows Script Components".

Aquí está a tutorial on creating WSCs.

Un componente de script de Windows es un componente COM que se define en el script. La interfaz del componente es a través de COM, lo que significa que es compatible con VBA. La lógica se implementa en cualquier lenguaje compatible con Windows Scripting Hosting, como JavaScript o VBScript. El WSC se define en un solo archivo XML, que integra la lógica, el componente ID de clase, los métodos, la lógica de registro, etc.

También hay un tool available to help in creating a WSC. Básicamente se trata de un tipo de asistente que le hace preguntas y rellena la plantilla XML. Yo mismo, comencé con un archivo .wsc de ejemplo y lo edité a mano con un editor de texto. Es bastante auto explicativo.

Un componente COM definido de esta manera en el script (en un archivo .wsc) se puede llamar como cualquier otro componente COM, desde cualquier entorno que pueda bailar con COM.

ACTUALIZACIÓN: Me tomé unos minutos y produje el WSC para GoogleDiff. Aquí está.

<?xml version="1.0"?> 

<package> 

<component id="Cheeso.Google.DiffMatchPatch"> 

    <comment> 
    COM Wrapper on the Diff/Match/Patch logic published by Google at http://code.google.com/p/google-diff-match-patch/. 
    </comment> 

<?component error="true" debug="true"?> 

<registration 
    description="WSC Component for Google Diff/Match/Patch" 
    progid="Cheeso.Google.DiffMatchPatch" 
    version="1.00" 
    classid="{36e400d0-32f7-4778-a521-2a5e1dd7d11c}" 
    remotable="False"> 

    <script language="VBScript"> 
    <![CDATA[ 

    strComponent = "Cheeso's COM wrapper for Google Diff/Match/Patch" 

    Function Register 
     MsgBox strComponent & " - registered." 
    End Function 

    Function Unregister 
     MsgBox strComponent & " - unregistered." 
    End Function 

    ]]> 
    </script> 
</registration> 


<public> 
    <method name="Diff"> 
    <parameter name="text1"/> 
    <parameter name="text2"/> 
    </method> 
    <method name="DiffFast"> 
    <parameter name="text1"/> 
    <parameter name="text2"/> 
    </method> 
</public> 


<script language="Javascript"> 
<![CDATA[ 


    // insert original google diff code here... 


// public methods on the component 
var dpm = new diff_match_patch(); 


function Diff(text1, text2) 
{ 
    return dpm.diff_main(text1, text2, false); 
} 


function DiffFast(text1, text2) 
{ 
    return dpm.diff_main(text1, text2, true); 
} 


]]> 
</script> 

</component> 

</package> 

Para usar eso, usted tiene que registrarlo. En Explorer, haz clic derecho sobre él y selecciona "Registrarse". o, desde la línea de comandos: archivo regsvr32: \ c: \ scripts \ GoogleDiff.wsc

No intenté usarlo desde VBA, pero aquí hay un código de VBScript que usa el componente.

Sub TestDiff() 
    dim t1 
    t1 = "The quick brown fox jumped over the lazy dog." 

    dim t2 
    t2 = "The large fat elephant jumped over the cowering flea." 

    WScript.echo("") 

    WScript.echo("Instantiating a Diff Component ...") 
    dim d 
    set d = WScript.CreateObject("Cheeso.Google.DiffMatchPatch") 

    WScript.echo("Doing the Diff...") 
    x = d.Diff(t1, t2) 

    WScript.echo("") 
    WScript.echo("Result was of type: " & TypeName(x)) 
    ' result is all the diffs, joined by commas. 
    ' Each diff is an integer (position), and a string. These are separated by commas. 
    WScript.echo("Result : " & x) 

    WScript.echo("Transform result...") 
    z= Split(x, ",") 
    WScript.echo("") 
    redim diffs(ubound(z)/2) 
    i = 0 
    j = 0 
    For Each item in z 
     If (j = 0) then 
     diffs(i) = item 
     j = j+ 1  
     Else 
      diffs(i) = diffs(i) & "," & item 
     i = i + 1 
     j = 0 
     End If 
    Next 

    WScript.echo("Results:") 
    For Each item in diffs 
     WScript.echo(" " & item) 
    Next 

    WScript.echo("Done.") 

End Sub 
+0

Impresionante. Voy a dar una oportunidad cuando tenga oportunidad. Mientras tanto, aceptaré esto como la mejor respuesta. – richardtallent

+0

Cerrar, pero diff_main devuelve una matriz de diferencias, cada una de las cuales es una matriz de dos elementos con el operador (igual, eliminado o insertado, como un entero) y el texto. Todavía estoy trabajando en cómo hacer que VBA trate el resultado como una matriz para poder avanzar y crear el formato adecuado en la celda de Excel. – richardtallent

+0

Sé lo que la lógica javascript PIENSA el valor devuelto. En mi prueba de VBScript, el tipo del valor de retorno es Cadena. Entonces en mi ejemplo vbscript, dividí la cadena y reconstruí la matriz de "diferencias". – Cheeso

2

Mi sugerencia es que hagas lo que hagas lo envuelves en un contenedor COM. VBA funciona mejor con objetos COM para que pueda compilar como un Componente .NET y luego exponer como un objeto COM utilizando la funcionalidad de interoperabilidad de .NET.

Como alternativa, también podría considerar el uso de objetos Windows Scripting Host para ejecutar un archivo Javascript y devolverle el resultado.

+0

Puede hacer AMBAS. Usando los componentes de Windows Script puede definir su componente COM en Javascript e invocar el componente COM de VBA o lo que sea. – Cheeso

4

El Windows Scripting Engine le permitirá ejecutar la biblioteca de JavaScript. Funciona bien en mi experiencia.

+0

Y al empaquetar la lógica de Javascript como un componente COM, a través de esto que Microsoft llama Windows Script Components, será fácil llamar al Javascript desde Excel/VBA. – Cheeso

1

Aquí hay otra opción a considerar, aunque de ninguna manera estoy diciendo que es la mejor.

  • Asegúrate de que la versión de Python compila en IronPython. (No debería haber ningún problema aquí, o solo una pequeña cantidad de portabilidad como máximo).
  • Cree una biblioteca de complementos de Excel usando C# y haga referencia a IronPython desde allí.
  • Envuelva la funcionalidad necesaria en su complemento C# Excel.
+0

Eso obtendría una solución _toda .Net_. Me gusta. –

+0

Me encantaría una solución all-.NET, PERO estoy atascado con Excel 2003. Además, mis usuarios pueden o no tener instalada una versión particular del tiempo de ejecución de .NET, por lo que se prefiere una solución completamente VBA. – richardtallent

+0

En ese caso, el motor de secuencias de comandos (basado en COM) debe ajustarse a la factura. –

Cuestiones relacionadas